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.
Discussion of the tutorials with friends and classmates is allowed, with acknowledgment, but please read the Moodle page on “Collaboration, plagiarism, and AI”.
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.
A partition of a positive number is a way of writing it as a sum of positive numbers, where by convention we order the parts from largest to smallest. Formally, a partition of an integer \(n\) is a sequence of positive integers \(p_1,\dots,p_k\) such that \(p_1 \ge \dots \ge p_k\) and \(n = p_1 + \dots + p_k\). For example, the number 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:
= [4]
p1 = [3, 1]
p2 = [2, 2]
p3 = [2, 1, 1]
p4 = [1, 1, 1, 1] p5
Each of these lists is reverse-sorted
(all (p[i] >= p[i+1] for i in range(len(p)-1))
) and sums
to 4 (sum(p) == 4
). Incidentally, observe that the above
listing of partitions is also reverse-sorted itself, 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 and ending with the all-1s partition.
- Start by setting
p = [n]
, and enter into a loop. - At each iteration, yield the current partition
p
and then try to find the largest indexi
such thatp[i] > 1
. - If there is no such index, that means that
p
is the all-1s partition[1, ..., 1]
, and we are done. - Otherwise,
p
consists of some prefixp[:i]
, followed by the valuep[i]
, followed by a suffixp[i+1:]
of trailing 1s. Thus we replacep[i]
byp[i]-1
, and replace the trailing 1s by as many possible repetitions ofp[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]
.
Implement the iterative algorithm as a generator function
parts_lex(n)
that successively yields all integer partitions ofn
, in reverse lexicographic order. You can assume thatn
is a positive integer.For example, you should be able to reproduce the output of the following session, which begins by printing all partitions of 4 and 7 in lexicographic order, and then counts the total numbers of partitions of the integers from \(n=1..30\).
1]: for p in parts_lex(4): print(p) In [4] [3, 1] [2, 2] [2, 1, 1] [1, 1, 1, 1] [2]: for p in parts_lex(7): print(p) In [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] [3]: [sum(1 for _ in parts_lex(n)) for n in range(1,31)] In [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] Out[
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
- \(p(n,k) = p(n-1,k-1) + p(n-k,k)\)
for all \(n>0\) and \(1 \le k \le n\), with the equations
- \(p(n,k) = 0\) if \(k > n\)
- \(p(n,0) = 0\) if \(n > 0\)
- \(p(0,0) = 1\)
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)\]
Translate the above relations directly into a recursive function
parts_cnt(n, k=None)
that counts partition numbers. In addition to a non-negative integern
as first parameter, it should takek
as a second parameter that is eitherNone
(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.
4]: parts_cnt(4) In [4]: 5 Out[5]: [parts_cnt(n) for n in range(31)] In [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] Out[6]: [parts_cnt(30,k) for k in range(31)] In [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] Out[
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:
starting with a partition \(n-1 = a_1 + \dots + a_{k-1}\), and adding a part \(a_k = 1\) to get \(n = a_1 + \dots + a_{k-1} + 1\); or
starting with a partition \(n-k = b_1 + \dots + b_k\), and incrementing each of the parts to get \(n = (1+b_1) + \dots + (1+b_k)\).
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.
Write a recursive generator function
parts_rec(n, k=None)
that successively yields all partitions of a non-negative integern
, into an arbitrary number of parts ifk
isNone
, or else into exactlyk
parts. The function shouldyield
each of the partitions as a reverse-sorted list of positive integers that sum ton
(and are of lengthk
ifk
is set), but it can yield them in an arbitrary order. (Unless you implementparts_rec
in a strange way, the order that it visits partitions will be different from the order implemented byparts_lex
, at least fromn = 6
or earlier.)7]: for p in parts_rec(4,2): print(p) In [3, 1] [2, 2] [8]: for p in parts_rec(7): print(p) In [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] [9]: for p in parts_rec(7,3): print(p) In [5, 1, 1] [4, 2, 1] [3, 3, 1] [3, 2, 2] [10]: [sum(1 for _ in parts_rec(n)) for n in range(31)] In [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] Out[
Hint: The structure of
parts_rec
should exactly mirror the structure ofparts_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 []
= randperm(n-1) # pick a random permutation of 1..n-1
pi = random.randrange(n) # pick a random index 0 <= i < n
i 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)
.
Theory question: Prove that
randperm(n)
returns a permutation of \(1\dots n\) uniformly at random, assuming thatrandom.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)
returnspi
is exactly \(1/n!\), for every permutationpi
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
.
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 thatrandom.choices
returns a list of length 1 by default.You are allowed to assume that
randpart
is only ever called with a non-negative integern <= 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:
11]: rpg = RandPartsGen(100) In [12]: rpg.randpart(100) In [12]: [13, 12, 8, 8, 7, 7, 7, 7, 7, 7, 4, 2, 2, 2, 2, 2, 2, 1] Out[13]: rpg.randpart(100, 5) In [13]: [52, 16, 11, 11, 10] Out[14]: rpg.randpart(10, 3) In [14]: [5, 3, 2] Out[15]: rpg = RandPartsGen(1000) In [16]: rpg.randpart(1000, 10) In [16]: [305, 216, 128, 100, 98, 57, 53, 28, 10, 5] Out[
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): tuple(p)] = 0 cnt[for _ in range(trials): = rpg.randpart(n,k) p tuple(p)] += 1 cnt[return cnt
And here is an example of a typical output:
17]: dist_randpart(rpg, 7) In [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} Out[18]: dist_randpart(rpg, 10, 3) In [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} Out[
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*}\]
Write a function
randopart_rej(rpg, n, k=None)
that uses rejection sampling in order to construct an odd partition ofn
withk
parts uniformly at random, using the random partition generatorrpg
. The output should be a pair(p, s)
of an odd partitionp
and an integers
that gives the number of partitions that had to be sampled fromrpg
to find an odd partition.Some examples (of course your outputs will differ):
19]: randopart_rej(rpg, 10) In [19]: ([5, 3, 1, 1], 2) Out[20]: randopart_rej(rpg, 100) In [20]: ([37, 31, 15, 3, 3, 3, 3, 3, 1, 1], 603) Out[21]: randopart_rej(rpg, 200) In [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) Out[
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!).
Following the recursive strategy sketched above, define a uniform random generator for odd partitions and even partitions, by completing the definitions of
__init__
,randopart
, andranddpart
in the classRandOddEvenPartsGen
below that extendsRandPartsGen
: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:
22]: rpg = RandOddEvenPartsGen(1000) In [23]: rpg.randopart(1000) In [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] Out[24]: rpg.randopart(1000, 20) In [24]: [201, 131, 109, 95, 91, 57, 51, 45, 41, 29, 21, 21, 21, 19, 15, 13, 13, 13, 13, 1] Out[25]: rpg.randepart(1000, 20) In [25]: [140, 120, 114, 106, 102, 86, 74, 74, 40, 22, 22, 18, 18, 18, 18, 14, 8, 2, 2, 2] Out[
(Following the model of
dist_randpart
above, you may want to write some tests to confirm that yourrandopart
andrandepart
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!
Write a function
odd_to_strict(p)
that takes as input a partition ofn = sum(p)
into odd parts, and returns a partition ofn
into distinct parts by applying Sylvester’s bijection.A few examples of the expected output:
26]: odd_to_strict([9,5,3,3,1]) In [26]: [9, 7, 4, 1] Out[27]: odd_to_strict([5,5,5,3,1,1,1]) In [27]: [9, 5, 4, 2, 1] Out[28]: odd_to_distinct([7,7,7]) In [28]: [6, 5, 4, 3, 2, 1] Out[
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:
29]: randspart(rpg, 100)
In [29]: [37, 22, 12, 9, 8, 6, 5, 1]
Out[30]: randspart(rpg, 100)
In [30]: [23, 22, 20, 19, 13, 3]
Out[31]: randspart(rpg, 1000)
In [31]: [106, 80, 74, 70, 64, 63, 62, 58, 50, 47, 42, 40, 39, 37, 35, 30, 25, 22, 21, 20, 15] Out[
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.
Define a new class
RandStrictPartsGen
equipped with a methodrandspart(self, n, k=None)
that returns a uniformly random partition ofn
withk
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.