You must be authenticated to submit your files

CSE 102 – Tutorial 9 – Combinatorial generation

Sections tagged with a green marginal line are mandatory.

Sections tagged with an orange marginal line are either more advanced or supplementary, and are therefore considered optional. They will help you deepen your understanding of the material in the course. You may skip these sections on a first pass through the tutorial, and come back to them after finishing the mandatory exercises.

After you are finished with the tutorial, please upload your submission to Moodle by the indicated deadline (end of the day Wednesday, May 22nd Friday, May 24th).

Generating integer partitions

We expect you to write your solution to this problem in a file named parts.py and to upload it using the form below. You can resubmit your solution as many times as you like.

Upload form is only available when connected

A partition of an integer \(n\) is a way of writing it as the sum of positive integers \(n = p_1 + \dots + p_k\), where \(p_1 \ge \dots \ge p_k\). For example, 4 has five distinct partitions:

\[\begin{align*} 4 &= 4 \\ 4 &= 3 + 1 \\ 4 &= 2 + 2 \\ 4 &= 2 + 1 + 1 \\ 4 &= 1 + 1 + 1 + 1 \end{align*}\]

We will represent partitions in Python as reverse-sorted lists of positive integers. Thus the five partitions of 4 are represented as the following lists:

p1 = [4]
p2 = [3, 1]
p3 = [2, 2]
p4 = [2, 1, 1]
p5 = [1, 1, 1, 1]

each of which is reverse-sorted (all (p[i] >= p[i+1] for i in range(len(p)-1))) and sums to 4 (sum(p) == 4). Observe that the above listing of partitions is itself reverse-sorted, in the sense that p1 > p2 > p3 > p4 > p5 relative to the lexicographic order on lists.

There is a simple iterative algorithm for enumerating partitions exhaustively in reverse lexicographic order, starting with the singleton partition [n] and ending with the all-1s partition [1, ..., 1]. At each iteration, we yield the current partition p and then try to find the largest index i such that p[i] > 1. If there is no such index, that means that p is the all-1s partition, and we are done. Otherwise, p consists of some prefix p[:i], followed by the value p[i], followed by a suffix p[i+1:] of trailing 1s. Thus we replace p[i] by p[i]-1, and replace the trailing 1s by as many possible repetitions of p[i]-1 as fit, followed by a remainder term if necessary.

For example, if we apply this procedure to enumerate the partitions of 7, then after the initial partition [7], the next four partitions are [6, 1], [5, 2], [5, 1, 1], and [4, 3].

  1. Implement the iterative algorithm as a generator function parts_lex(n) that successively yields all integer partitions of n, in reverse lexicographic order. You can assume that n is a positive integer.

    In [1]: for p in parts_lex(4): print(p)
    [4]
    [3, 1]
    [2, 2]
    [2, 1, 1]
    [1, 1, 1, 1]
    In [2]: for p in parts_lex(7): print(p)
    [7]
    [6, 1]
    [5, 2]
    [5, 1, 1]
    [4, 3]
    [4, 2, 1]
    [4, 1, 1, 1]
    [3, 3, 1]
    [3, 2, 2]
    [3, 2, 1, 1]
    [3, 1, 1, 1, 1]
    [2, 2, 2, 1]
    [2, 2, 1, 1, 1]
    [2, 1, 1, 1, 1, 1]
    [1, 1, 1, 1, 1, 1, 1]
    In [3]: [sum(1 for _ in parts_lex(n)) for n in range(1,31)]
    Out[3]: [1, 2, 3, 5, 7, 11, 15, 22, 30, 42, 56, 77, 101, 135, 176, 231, 297, 385, 490, 627, 792, 1002, 1255, 1575, 1958, 2436, 3010, 3718, 4565, 5604]

The goal of the next series of exercises is to write a function that generates partitions of an integer uniformly at random. We’ll begin by considering how to enumerate partitions recursively.

The number of partitions of \(n\) is denoted \(p(n)\), also known as the partition function. For example, \(p(4) = 5\) as we saw above. To compute \(p(n)\), it is convenient to introduce more refined partition numbers \(p(n,k)\) counting the number of partitions of \(n\) into \(k\) parts. For example, looking at the above listing, we see that \(p(4,2) = 2\) since there are two partitions of four into two parts ([3, 1] and [2, 2]), and \(p(4,3) = 1\) since there is only one partition of four into three parts ([2, 1, 1]). The numbers \(p(n,k)\) can be computed recursively via the equation

for all \(n>0\) and \(1 \le k \le n\), with the equations

covering the remaining cases. The total number of partitions of \(n\) can then be computed by the formula

\[p(n) = \sum_{k=0}^n p(n,k)\]

  1. Translate the above relations directly into a recursive function parts_cnt(n, k=None) that counts partition numbers. In addition to a non-negative integer n as first parameter, it should take k as a second parameter that is either None (the default) or a non-negative integer, returning \(p(n)\) in the first case and \(p(n,k)\) in the second.

    Note that although the recurrence relation for \(p(n,k)\) leads to overlapping subproblems, we are not asking you to use dynamic programming for this problem — at least not yet. For example, all of the following computations should return within a fraction of a second, but for larger values of \(n\) the computation of the partition numbers will start to slow down noticeably.

    In [4]: parts_cnt(4)
    Out[4]: 5
    In [5]: [parts_cnt(n) for n in range(31)]
    Out[5]: [1, 1, 2, 3, 5, 7, 11, 15, 22, 30, 42, 56, 77, 101, 135, 176, 231, 297, 385, 490, 627, 792, 1002, 1255, 1575, 1958, 2436, 3010, 3718, 4565, 5604]
    In [6]: [parts_cnt(30,k) for k in range(31)]
    Out[6]: [0, 1, 15, 75, 206, 377, 532, 618, 638, 598, 530, 445, 366, 290, 229, 176, 135, 101, 77, 56, 42, 30, 22, 15, 11, 7, 5, 3, 2, 1, 1]

Your next task will be to write a generator function that goes beyond counting to explicitly enumerate all partitions of an integer recursively. For this, it is important to understand where the recurrence relation and boundary cases for the partition numbers \(p(n,k)\) come from. When \(n > 0\) and \(1 \le k \le n\), the recurrence relation

\[p(n,k) = p(n-1,k-1) + p(n-k,k)\]

can be established by the following argument: a partition of \(n\) into \(k\) parts either has smallest part 1, in which case the rest is a partition of \(n-1\) into \(k-1\) parts, or else its smallest part is at least 2, and we can subtract 1 from each part to obtain a partition of \(n-k\) into \(k\) parts. In other words, assuming \(n > 0\) and \(1\le k\le n\), we can obtain any partition \(n = a_1 + \dots + a_k\) of \(n\) into \(k\) parts by either:

In the boundary cases, we have

\[p(n,k) = 0\text{ if }k > n \qquad \text{and} \qquad p(n,0) = 0\text{ if }n > 0\]

because there is no way to express \(n\) as a sum of more than \(n\) positive integers, or to express it as an empty sum if \(n>0\). Finally, we do have

\[p(0,0) = 1\]

because the empty partition, i.e., the empty sum, is a valid partition of 0 into 0 parts.

  1. Write a recursive generator function parts_rec(n, k=None) that successively yields all partitions of a non-negative integer n, into an arbitrary number of parts if k is None, or else into exactly k parts. The function should yield each of the partitions as a reverse-sorted list of positive integers that sum to n (and are of length k if k is set), but it can yield them in an arbitrary order. (Unless you implement parts_rec in a strange way, the order that it visits partitions will be different from the order implemented by parts_lex, at least from n = 6 or earlier.)

    In [7]: for p in parts_rec(4,2): print(p)
    [3, 1]
    [2, 2]
    In [8]: for p in parts_rec(7): print(p)
    [7]
    [6, 1]
    [5, 2]
    [4, 3]
    [5, 1, 1]
    [4, 2, 1]
    [3, 3, 1]
    [3, 2, 2]
    [4, 1, 1, 1]
    [3, 2, 1, 1]
    [2, 2, 2, 1]
    [3, 1, 1, 1, 1]
    [2, 2, 1, 1, 1]
    [2, 1, 1, 1, 1, 1]
    [1, 1, 1, 1, 1, 1, 1]
    In [9]: for p in parts_rec(7,3): print(p)
    [5, 1, 1]
    [4, 2, 1]
    [3, 3, 1]
    [3, 2, 2]
    In [10]: [sum(1 for _ in parts_rec(n)) for n in range(31)]
    Out[10]: [1, 1, 2, 3, 5, 7, 11, 15, 22, 30, 42, 56, 77, 101, 135, 176, 231, 297, 385, 490, 627, 792, 1002, 1255, 1575, 1958, 2436, 3010, 3718, 4565, 5604]

    Hint: The structure of parts_rec should exactly mirror the structure of parts_cnt, but whereas the latter calls itself recursively to compute the partition number \(p(n,k)\) as a sum of smaller partition numbers, parts_rec should call itself recursively to generate partitions of smaller numbers, and then transform those into partitions of \(n\). You should also be careful to deal with the boundary cases correctly: when there are no partitions, your function should yield nothing, and when there is only the empty partition, your function should yield [] and return.

Now, as discussed in Lecture 9, it is possible to turn a recursive enumerator into a random generator by replacing exhaustion with random choice. For example, the generator function perms_rec(n) below implements a recursive strategy for enumerating all permutations of the numbers \(1\dots n\), and randperm(n) uses a corresponding strategy to compute a single random permutation.

def perms_rec(n):
    if n == 0:
        yield []
        return
    for pi in perms_rec(n-1):            # iterate over all permutation of 1..n-1
        for i in range(n):               # iterate over all indices 0 <= i < n
            yield pi[:i] + [n] + pi[i:]  # build a permutation of 1..n

def randperm(n):
    if n == 0:
        return []
    pi = randperm(n-1)                   # pick a random permutation of 1..n-1
    i = random.randrange(n)              # pick a random index 0 <= i < n
    return pi[:i] + [n] + pi[i:]         # build a permutation of 1..n

This example is a bit special in that we immediately obtained a uniform random generator without any extra work: one can verify that every permutation of \(1\dots n\) has equal probability of being returned by randperm(n).

  1. Theory question: Prove that randperm(n) returns a permutation of \(1\dots n\) uniformly at random, assuming that random.randrange(n) returns an integer in the range \(0\dots n-1\) uniformly at random.

    Hint: by induction on \(n\), prove that the probability that randperm(n) returns pi is exactly \(1/n!\), for every permutation pi of \(1 \dots n\).

In the case of permutations we could use uniform choice when choosing the values of the random variables pi and i, but more generally, we need to use biased sampling to turn a recursive enumerator into a uniform random generator, weighing the different possible values for each random variable by the probability that a random object has that value. (Question: so why was it okay to select pi and i using uniform sampling?)

For the concrete case of partitions it works as follows. In the recursive construction of a partition of \(n\) with \(k\) parts, to generate a random partition we need to decide whether to construct one from a partition of \(n-1\) into \(k-1\) parts, or from a partition of \(n-k\) into \(k\) parts. But we should not decide by flipping a fair coin! Instead we should flip a biased coin, choosing the first construction with probability \(\frac{p(n-1,k-1)}{p(n,k)}\). Similarly, to generate a random partition of \(n\) with an arbitrary number of parts, we can pick \(k\) at random between \(0\) and \(n\), and then generate a random partition of \(n\) into \(k\) parts. But to get a uniform generator, we must not choose \(k\) uniformly within this range. Rather, we should choose the value \(k\) with probability \(\frac{p(n,k)}{p(n)}\). (For instance, when \(n>0\), we should never pick \(k=0\) since \(p(n,0) = 0\), and we should only pick \(k=1\) with probability \(1/p(n)\). The probability that a random partition of \(n=100\) has exactly one part is about 1 in 190 million.)

In order to apply this strategy to generate large random integer partitions (say, with \(n=1000\)), we need to be able to compute partition numbers efficiently, and for that it’s important to use dynamic programming. Moreover, since we may want to sample random partitions repeatedly, it makes sense to precompute a table of partition numbers in advance. To achieve this, we’ll bundle the random generator into a class RandPartsGen equipped with a constructor RandPartsGen(maxn) that takes an upper bound on the size of the partitions and precomputes a table of partition numbers. The constructed RandPartsGen object is equipped with a method randpart(n, k=None) that returns a uniformly random partition of the given integer n (optionally, with a given number of parts k), assuming that n <= maxn.

  1. Define a random partition generator by completing the definition of the class RandPartsGen below, as per the description above.

    class RandPartsGen:
        def __init__(self, maxn):
            '''Initialize a random generator of partitions of integers n <= maxn.'''
            pass
    
        def randpart(self, n, k=None):
            '''Assuming 0 <= n <= self.maxn, returns a uniformly random partition of n with k parts.
            If there is no such partition, returns None.'''
            pass

    In order to get a uniform generator, you should use biased sampling with probabilities determined by the partition counts, as explained above. You wrote code for simulating biased die rolls in Tutorial 4, and you can feel free to reuse that code, or to use the function random.choices(population, weights) from the random module in the Python Standard Library, which implements weighted sampling. Note that random.choices returns a list of length 1 by default.

    You are allowed to assume that randpart is only ever called with a non-negative integer n <= self.maxn — though in a practical library implementation you might want to check this condition explicitly and either raise an exception or dynamically grow the table of partition numbers.

    Here are some example uses:

    In [11]: rpg = RandPartsGen(100)
    In [12]: rpg.randpart(100)
    Out[12]: [13, 12, 8, 8, 7, 7, 7, 7, 7, 7, 4, 2, 2, 2, 2, 2, 2, 1]
    In [13]: rpg.randpart(100, 5)
    Out[13]: [52, 16, 11, 11, 10]
    In [14]: rpg.randpart(10, 3)
    Out[14]: [5, 3, 2]
    In [15]: rpg = RandPartsGen(1000)
    In [16]: rpg.randpart(1000, 10)
    Out[16]: [305, 216, 128, 100, 98, 57, 53, 28, 10, 5]

    To make sure that your random generator is correct, you can collect the output of calling it many times for small fixed values of \(n\) and \(k\), and verify that every partition of \(n\) with \(k\) parts occurs with roughly equal frequency. Here is a function that calls the random partition generator repeatedly and returns a dictionary of partition counts (the coercions tuple(p) are necessary because lists are not hashable in Python):

    def dist_randpart(rpg, n, k=None, trials=10000):
        cnt = {}
        for p in parts_rec(n,k):
            cnt[tuple(p)] = 0
        for _ in range(trials):
            p = rpg.randpart(n,k)
            cnt[tuple(p)] += 1
        return cnt

    And here is an example of a typical output:

    In [17]: dist_randpart(rpg, 7)
    Out[17]: {(7,): 685, (6, 1): 689, (5, 2): 671, (4, 3): 695, (5, 1, 1): 687, (4, 2, 1): 639, (3, 3, 1): 670, (3, 2, 2): 640, (4, 1, 1, 1): 690, (3, 2, 1, 1): 654, (2, 2, 2, 1): 672, (3, 1, 1, 1, 1): 628, (2, 2, 1, 1, 1): 658, (2, 1, 1, 1, 1, 1): 678, (1, 1, 1, 1, 1, 1, 1): 644}
    In [18]: dist_randpart(rpg, 10, 3)
    Out[18]: {(8, 1, 1): 1264, (7, 2, 1): 1257, (6, 3, 1): 1191, (5, 4, 1): 1243, (6, 2, 2): 1283, (5, 3, 2): 1293, (4, 4, 2): 1260, (4, 3, 3): 1209}

For the next series of optional problems, we consider generation of partitions with some additional restrictions. A partition is said to be “odd” if it contains only odd integer parts. For example, 7 has five odd partitions: \[\begin{align*} 7 &= 7 \\ 7 &= 5 + 1 + 1 \\ 7 &= 3 + 3 + 1 \\ 7 &= 3 + 1 + 1 + 1 + 1 \\ 7 &= 1 + 1 + 1 + 1 + 1 + 1 + 1 \end{align*}\]

  1. Write a function randopart_rej(rpg, n, k=None) that uses rejection sampling in order to construct an odd partition of n with k parts uniformly at random, using the random partition generator rpg. The output should be a pair (p, s) of an odd partition p and an integer s that gives the number of partitions that had to be sampled from rpg to find an odd partition.

    Some examples (of course your outputs will differ):

    In [19]: randopart_rej(rpg, 10)
    Out[19]: ([5, 3, 1, 1], 2)
    In [20]: randopart_rej(rpg, 100)
    Out[20]: ([37, 31, 15, 3, 3, 3, 3, 3, 1, 1], 603)
    In [21]: randopart_rej(rpg, 200)
    Out[21]: ([43, 29, 13, 13, 13, 11, 7, 7, 7, 7, 7, 7, 5, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 6105)

Rejection sampling of odd partitions is inefficient, because as \(n\) grows larger a random partition of \(n\) is very unlikely to contain only odd parts. A much better approach is to generate them recursively, following a similar strategy to the one we used for unrestricted partitions, but now defining odd partitions in mutual recursion with even partitions (i.e., partitions containing only even integer parts). Indeed, if we let \(p_o(n,k)\) and \(p_e(n,k)\) stand for the numbers of odd and even partitions of \(n\) into \(k\) parts, respectively, then these partition counts satisfy the recurrence relations

\[\begin{align*} p_o(n,k) &= p_o(n-1,k-1) + p_e(n-k,k) \\ p_e(n,k) &= p_o(n-k,k) \end{align*}\]

for all \(n>0\) and \(1 \le k \le n\). We can argue for the recurrence relations as follows. A partition of \(n\) into \(k\) odd parts either has smallest part 1, in which case the rest is a partition of \(n-1\) into \(k-1\) odd parts, or else its smallest part is at least 3, and we can subtract 1 from each part to obtain a partition of \(n-k\) into \(k\) even parts. Hence \(p_o(n,k) = p_o(n-1,k-1) + p_e(n-k,k)\). On the other hand, a partition of \(n\) into \(k\) even parts, for \(1 \le k \le n\), necessarily has smallest part at least 2, and so we can subtract 1 from each part to obtain a partition of \(n-k\) into \(k\) odd parts. Hence \(p_e(n,k) = p_o(n-k,k)\). Finally, the odd and even partition numbers satisfy the same boundary cases as for the ordinary partition numbers \(p(n,k)\), that is \(p_o(n,k) = p_e(n,k) = 0\) if \(k > n\) or if \(n > 0\) and \(k = 0\) (since there are no partitions in these cases, and hence no odd/even partitions), and \(p_o(0,0) = p_e(0,0) = 1\) (since, vacuously, all of the parts in the empty partition are both even and odd!).

  1. Following the recursive strategy sketched above, define a uniform random generator for odd partitions and even partitions, by completing the definitions of __init__, randopart, and randdpart in the class RandOddEvenPartsGen below that extends RandPartsGen:

    class RandOddEvenPartsGen(RandPartsGen):
        def __init__(self, maxn):
            super().__init__(maxn)  # initialization for RandPartsGen
            pass                    # some more initialization...
    
        def randopart(self, n, k=None):
            '''Assuming 0 <= n <= self.maxn, returns a uniformly random partition of n with k odd parts.
                If there is no such partition, returns None.'''
            pass
    
        def randepart(self, n, k=None):
            '''Assuming 0 <= n <= self.maxn, returns a uniformly random partition of n with k even parts.
                If there is no such partition, returns None.'''
            pass

    After a small amount of time to precompute the tables during initialization, your random partition generator should now be able to generate odd/even partitions of size \(n=1000\) with any number of parts very efficiently. Some typical outputs:

    In [22]: rpg = RandOddEvenPartsGen(1000)
    In [23]: rpg.randopart(1000)
    Out[23]: [131, 129, 75, 55, 37, 27, 25, 23, 17, 17, 17, 17, 17, 17, 17, 11, 11, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    In [24]: rpg.randopart(1000, 20)
    Out[24]: [201, 131, 109, 95, 91, 57, 51, 45, 41, 29, 21, 21, 21, 19, 15, 13, 13, 13, 13, 1]
    In [25]: rpg.randepart(1000, 20)
    Out[25]: [140, 120, 114, 106, 102, 86, 74, 74, 40, 22, 22, 18, 18, 18, 18, 14, 8, 2, 2, 2]

    (Following the model of dist_randpart above, you may want to write some tests to confirm that your randopart and randepart methods truly produce uniform distributions over odd and even partitions. Please include these tests in your submission file.)

Another natural restriction that we may place on integer partitions is that all of the parts be distinct. For example, there are five ways of writing 7 as a sum of distinct positive numbers: \[\begin{align*} 7 &= 7 \\ 7 &= 6 + 1 \\ 7 &= 5 + 2 \\ 7 &= 4 + 3 \\ 7 &= 4 + 2 + 1 \end{align*}\] A partition \(n = p_1 + \dots + p_k\) into distinct parts \(p_1 > \dots > p_k\) is also called a strict partition of \(n\). Once again, we could try to use rejection sampling to generate strict partitions uniformly at random, but this is inefficient because a large random partition is very likely to have repeated parts.

Fortunately, it is possible to turn a uniform generator of odd partitions into a uniform generator of strict partitions! Back in the mid-1700s, Euler proved that the number of partitions of \(n\) with odd parts is always equal to the number of partitions of \(n\) with distinct parts. Although Euler’s proof used purely algebraic manipulation, later J. J. Sylvester gave a proof of a (strengthening of) Euler’s theorem by describing an explicit bijection between partitions with odd parts and partitions with distinct parts. In general, if two sets of objects \(X\) and \(Y\) are in bijection \(X \rightleftarrows Y\), then we can turn an efficient uniform generator of \(X\)s into an efficient uniform generator of \(Y\)s and vice versa — at least assuming that the bijection is efficiently computable.

Sylvester’s bijection is best described using a graphical representation of partitions known as Young tableau or Ferrers diagrams. The idea is to represent each part by a row of boxes (or dots), stacked one on top of the other in descending order. For example, here is a representation of the (odd) partition \(21 = 9 + 5 + 3 + 3 + 1\) as a tableau:

To describe the bijection, let’s begin by placing a black square in every other box of the first row, as well as all along the first column:

The first part of the new partition will be 9, corresponding to the number of black squares. Next we remove the first column, so that what remains is an even partition (\(8 + 4 + 2 + 2 = 16\)):

This time we place a red square in every other box of the first row and all along the first column:

The second part of the new partition will be 7, corresponding to the number of red squares. We remove the first column and the first row and repeat the process until there are no more boxes, as illustrated below:

In the end we’ve obtained a partition of 21 into distinct parts \(21 = 9 + 7 + 4 + 1\). Observe though that the number of parts is not preserved!

  1. Write a function odd_to_strict(p) that takes as input a partition of n = sum(p) into odd parts, and returns a partition of n into distinct parts by applying Sylvester’s bijection.

    A few examples of the expected output:

    In [26]: odd_to_strict([9,5,3,3,1])
    Out[26]: [9, 7, 4, 1]
    In [27]: odd_to_strict([5,5,5,3,1,1,1])
    Out[27]: [9, 5, 4, 2, 1]
    In [28]: odd_to_distinct([7,7,7])
    Out[28]: [6, 5, 4, 3, 2, 1]
  2. Theory question: prove that odd_to_strict defines a bijection between odd partitions of \(n\) with \(k+1\) distinct values (possibly repeated) and strict partitions of \(n\) with \(k\) “gaps”, in the sense of consecutive parts \(p_i,p_{i+1}\) such that \(p_i - p_{i+1} \ge 2\). (For example, the odd partition \(21 = 9 + 5 + 3 + 3 + 1\) has four distinct values, and the corresponding strict partition \(21 = 9 + 7 + 4 + 1\) has three gaps. Similarly the odd partition \(21 = 5 + 5 + 5 + 3 + 1 + 1 + 1\) has three distinct values, and the corresponding strict partition \(21 = 9 + 5 + 4 + 2 + 1\) has two gaps.)

Now that you’ve implemented the bijection, it is easy to get a uniform generator of strict partitions, simply by composing with your uniform generator of odd partitions:

def randspart(rpg, n):
    return odd_to_strict(rpg.randopart(n))

Some typical outputs:

In [29]: randspart(rpg, 100)
Out[29]: [37, 22, 12, 9, 8, 6, 5, 1]
In [30]: randspart(rpg, 100)
Out[30]: [23, 22, 20, 19, 13, 3]
In [31]: randspart(rpg, 1000)
Out[31]: [106, 80, 74, 70, 64, 63, 62, 58, 50, 47, 42, 40, 39, 37, 35, 30, 25, 22, 21, 20, 15]

Again, you should write some tests to confirm that the output of randspart is truly uniform over strict partitions! It will be, assuming that you’ve implemented odd_to_strict and randopart correctly.

Finally, as we remarked above, Sylvester’s bijection does not preserve the number of parts, and so composing the uniform generator randopart(n, k) with odd_to_strict will not produce a uniform generator of strict partitions of n with k parts. However, it is still possible to generate such partitions uniformly at random, by another approach.

  1. Define a new class RandStrictPartsGen equipped with a method randspart(self, n, k=None) that returns a uniformly random partition of n with k distinct parts.

    Hint: Let \(p_s(n,k)\) denote the number of strict partitions of \(n\) with \(k\) parts. Devise a recurrence relation for the strict partition numbers \(p_s(n,k)\), perhaps in combination with the partition numbers for another family of partitions, and use it to directly generate strict partitions recursively, following a similar strategy to the ones for general partitions and for odd/even partitions.