Throwing a die for a cookie; Acceptance-rejection policies

1. Introduction

We have a class with \(N=19\) children and one biscuit. We would like to select one child to give the biscuit, but fate should decide which child. Can we use a fair six-sided die to select a winner, with multiple throws, such that all children have an equal probability of winning? And if so, what strategy would be best, in the sense that it minimizes the expected number of throws?

To answer these questions, we start with the analysis of simple cases to learn, hopefully, how to handle the case with \(19\) children. In this process we design a simple and intuitive strategy. As this policy is somehwat hard to analyze mathematically, we'll use simulation to assess its quality. It turns out that this policy is unfair: some children will have a larger probability of winning than others. Then we retract a bit and provide a better algorithm, which is fair and optimal in the sense that is requires the least number of throws on average to select the winner. {{{sidenote{(I discussed this problem with Henk Tijms and he pointed out an answer on Quora on how to handle a class with 20 pupils. Bases on this, I generalized this answer to an algorithm that can deal with an arbitrary number of children.)}}}

In this section we introduce some general concepts that are generally useful. The first is recursion, the second is to use acceptance-rejection policies to decide whether a game should continue or stop.

2. A general approach

The first aim is to find a policy that selects in a fair way a winner. But before embarking on how to do this for general classes, we start with considering some simple problems. Perhaps we spot a pattern!

Obviously, when there is \(N=1\) child, there is no need to choose. The next simplest case is when we have \(N=6\) children: number the children from 1 to 6, and throw the die.

What strategy would you use for \(N=2\) children?

Solution

Throw the die. If the outcome is in \(\{1, 2, 3\}\) give the biscuit to the first child, otherwise to the second.


What strategy would you use for \(N=3\)?

Solution

If the outcome is in \(\{1, 2\}\), give the biscuit to the first child; if in \(\{3, 4\}\) to the second; otherwise to the third.

When \(N=4\), we can use the following policy. Throw the die for each child. Each child whose outcome is below \(5\) `survives' to the next round. When there are no survivors after the first round (all threw \(5\) or \(6\) ) let them all throw once again. When there are \(1\), \(2\), or \(3\) survivors, use the suitable strategy we developed above. Thus, the idea of this algorithm is like this: use the die to reduce the group such that a set of children remains for which we already have a policy. However, while this is a fool-proof strategy, it requires at least \(4\) throws. We'd better search for more efficient policies.

For \(N=4\), find a policy that uses precisely two throws of the die to select a child.

Solution

Throw once. If the outcome is in \(\{1,2,3\}\), then we will use the second throw to give the cookie to either child \(1\) or \(2\). Instead, if the outcome is in \(\{4,5,6\}\), then we will use the second throw to give the cookie to either child \(3\) or \(4\).

Note that the first throw splits the group into two subgroups \(\{1,2\}\) and \(\{3, 4\}\) of the same size. Then we have two smaller groups, and for these smaller group we already know how to find a fair solution. Clearly, the pattern of recursion is to try to express the solution in terms of solutions of problems we solved earlier.

For \(N=4\) we can also keep on throwing until a child is selected. How would such a strategy work?

Solution

Throw the die once. If the outcome is in \(\{1, 2, 3, 4\}\), there is a winner. Otherwise, when the outcome is \(5\) or \(6\), throw again, and continue throwing until the outcome \(X\in \{1, 2, 3, 4\}\).

This second strategy is known as an acceptance-rejection policy: accept an outcome of an experiment when the outcome lies in a certain subset of the state space, otherwise reject the outcome, and sample again.1

We can compute the expected number of throws for the second policy with, so-called, first-step analysis. Write \(Y\) for the number of throws we need for the die to hit the set \(\{1,2,3,4\}\). The probability to hit this set is \(p=2/3\), while the probability to hit \(\{5,6\}\) is \(q=1-p\).

If \(\E Y\) is the expected number of throws until we are successful, explain that it must satisfy the relation \(\E Y = 1 + q \E Y\).

Solution

We need at least one throw. In case of a success, we can stop. In case of a failure, which happens with probability \(q\), we need another throw.

In probability terms, we say that \(Y\) has the first success distribution with failure probablity \(q\). This is rv is similar to a geometric rv, but slightly different. The first success rv counts the number of throws up and including the number of throws until a success; the geometric rv counts the number of failures until the success, so one less.2 From this,3

\begin{equation*} \E Y = 1 + q \E Y \implies (1-q) \E Y = 1 \implies \E Y = \frac{1}{1-q} = \frac{1}{p}. \end{equation*}

So, for \(N=4\), \(\E Y = 3/2\).

Interestingly, the first strategy, based on recursion, always needs exactly \(2\) throws. Instead, the acceptance-rejection method requires less throws on average, but there is no formal bound on \(Y\). We remark in passing an important take away in the design of random algorithms. Sometimes it is required that an algorithm finishes in a guaranteed fixed amount time, rather than that it finishes fast. When time constraints are relevant, it's good to have a deterministic algorithm, even though it can be slower on average.

What is the performance of the acceptance-rejection strategy for \(N=5\)?

Solution

The success probability \(p=5/6\), hence \(\E Y = 6/5\).


For \(N=7\), design an acceptance-rejection algorithm that uses rounds, each consisting of two throws.

Solution

We need at least one round of two throws. Let's make the acceptance region as large as possible so that the probability of rejection set is as small as possible. As \(7\times 5 = 35\), we should chop up the sample space in \(7\) non-overlapping sets of equal size. Let \(X_1\) and \(X_2\) be the outcomes of the first and second throw, respectively. When \(X_1=1, X_2\leq 5\), that is, when the outcome of both throws lies in \(\{(1,1), (1,2), (1,3), (1,4), (1,5)\}\), child 1 wins. When \(X_1=2, X_2 \leq 5\), child 2 wins, and so on until child 6. For child \(7\), take the associated set such that \(X_1 \leq 5, X_2 = 6\). Observe that these events don't overlap. Finally, if the outcome is \((6,6)\), rethrow the die.


For \(N=7\), what is the expected number of throws for this acceptance-rejection policy?

Solution

With success probability \(p=35/36\), one of the children is selected. If the outcome is \((6,6)\) we need another round of two throws. The expected number of rounds is \(1/p=36/35\). Since each round contains two throws, the number of throws is \(\E Y = 2/p = 72/35\), which is nearly \(2\).


For \(N=19\), what is the expected number of throws under an acceptance-rejection policy similar to the one of 1?

Solution

Throw twice. Assign student one to \((1,1)\), student two to \((1,2)\), \ldots, student \(19\) to \((4,1)\). Either a student is selected by the pair of throws, with success probability \(p=19/36\), or none is selected and we throw twice again. Then \(\E Y = 2/p = 72/19 \approx 4\).


Why is this form of acceptance-rejection much more efficient for classes of size \(N=7\) than when \(N=19\)?

Solution

For \(N=7\) we can cover the sample space nearly, the rejection set contains just one element. However, for \(N=19\), the cover of the state space with 36 outcomes is maximally inefficient.


Use the idea of the previous exercise to design a policy that needs slightly over 3 throws on average to select a winner for \(N=19\).

Solution

Take rounds, each with three throws, to get a sample space of size \(6^{3} = 216\) states. Split this into 19 subsets of equal size to guarantee that all children have the same probability to win, but such that the rejection is minimal. Since \(19\times 11 = 209\) each subset should contain 11 samples. For instance, for child 1 we can take \((1,1,1), (1,1,2), \ldots, (1, 2, 5)\).

The rejection set, i.e., the number of states not assigned to any child, is \(216-209=7\). Thus, the probability to need another rounds is \(7/209\), and the expected number of rounds is \(216/209\). Hence, the expected number of throws \(\E Y = 3 \times 216/209 \approx 3\).

Observe that with the strategy discussed in 1 for \(N=19\), we always have to throw at least three times. However, the strategy of 1 sometimes completes in just two throws. Might there be a optimal policy that needs somewhere between \(2\) and \(3\) throws on average?

3. An appealing, but wrong, strategy

One promising idea to improve on the strategy of using complete rounds of two throws for \(N=19\) is as follows. If there is no winner after the first and second throw, just throw the die a third time. Now check whether the result of the second and third throw is in the list of winning outcomes. To illustrate, if \((X_1, X_2) = (4, 3)\), then, by 1, the outcome \((4,3)\) is not a winning sequence, so that we have to continue with a third throw. After seeing \(X_3=4\), there is winning sequence, since \((X_2, X_3) = (3,4)\), which corresponds to child \((3-1)\cdot 6 + 4 = 16\). If, however, \((X_1, X_2, X_3) = (4, 5, 1)\) there is not yet a winner, because neither \((4,5)\) nor \((5,1)\) correspond to a child. Supposing that \(X_{4}=3\), the result of the last two throws is \((X_3, X_4) = (1, 3)\) so child \(3\) wins.

Notwithstanding that this is an appealing strategy, I was not convinced that this strategy will result in all children having an equal probability to win. The reason is that it reminded me of the, so-called, `monkey-typing problem'. In a later chapter we will explore this problem, but since its analysis requires some new mathematical ideas, we will use here simulation to see how our this new `monkey' policy performs when \(N=19\).

Before we discuss the code below, you should know that in Python (like in C and many other programming languages) arrays start at index 0. This will turn out very practical later in this chapter when we use modulo computions. Hence, in all maths and the code below, children start couting at \(0\), and if we have, for example \(4\) children, the associated set will be \(\{0, 1, 2, 3\}\), and not \(\{1, 2, 3, 4\}\).

The main parts of the next python code work as follows. The tuple last_two corresponds to the outcomes of the last two throws. The rejection set reject contains the tuples that do not correspond to a winner, hence require at least one further throw; hence, reject contains the outcome pairs \((3,1), \ldots, (5,5)\). If last_two is an element of reject, we throw the die again and update last_two to include the last throw. We continue with throwing until last_two is accepted. Once we have a winner, we add one win for this child. Finally, the count object counts how often each outcome occurs.4

import collections
import itertools

import numpy as np

rng = np.random.default_rng(1)

reject = set().union((3, i) for i in range(1, 6))
for i, j in itertools.product([4, 5], range(6)):
    reject.add((i, j))

count = collections.Counter()
num_runs = 100_000
for _ in range(num_runs):
    last_two = (rng.integers(6), rng.integers(6))
    while last_two in reject:
        last_two = (last_two[1], rng.integers(6))
    count[last_two] += 1
most = count.most_common()[0][1]
least = count.most_common()[-1][1]
mean = num_runs // 19

print(f"{len(reject)=}, {len(count)=}")
print(f"{least=}, {mean=}, {most=}, {count.total()=}")
len(reject)=17, len(count)=19
least=4464, mean=5263, most=5677, count.total()=100000

We print three checks: the size of the rejection set is \(17\), as it should, the counter counted \(19 = 35 - 17\) different outcomes, and we see \(10^5\) samples in total. The difference between the children that win the most and the least is about 900, i.e., about \(1\%\) for a sample size of \(10^{5}\) throws. Is this large? To provide some context, we can compare this with a simulation of uniform numbers on \(\{0, 1, \ldots, 18\}\).

import numpy as np

rng = np.random.default_rng(1)
N = 100_000
X = rng.integers(19, size=N)
bins = np.bincount(X)
print(f"{min(bins)=}, {max(bins)=}")
min(bins)=5144, max(bins)=5363

This variation between the numbers that occur the most and least often is quite a bit smaller than the variation under the monkey strategy. We can do further statistical tests, but for the moment I simply conclude that, under the monkey-typing strategy, the children don't have uniformly distributed winning chances.

Still I find it interesting to analyze how many throws this monkey strategy needs on average tof inish, in particular how it compares to the acceptance-rejection policy designed in 1. If it slower, then the policy to use (multiple) rounds of three throws times is better in all respects, but if the monkey policy is faster, we might make a trade off between fairness and speed. Note that this is a common theme in optimization: nearly always there are multiple objectives that we like to minimize or maximize, but we cannot perform optimally on all dimenstions at the same time. If so, we need to make a choice which objective we prefer over another.5

Returning to the analysis of the average number of throws it takes the monkey to select a winner, consider the code below. The next exercise asks you to explain some parts of it.

T = 0
for _ in range(N):
    last_two = (rng.integers(6), rng.integers(6))
    T += 2
    while last_two in reject:
        last_two = (last_two[1], rng.integers(6))
        T += 1

print(f"{T/N=}")
T/N=2.94335

Explain why T counts the total number of throws for N simulations.

Solution

We add \(2\) to capture the first two throws for each new game. Then we add a throw for each time last_two lies in the rejection set, hence did not lead to a winner.

As T counts the total number of throws for \(N\) games, T/N is the average number of throws per game. Apparently, this lies just a bit below \(3\). In summary, although unfair, the monkey policy needs less throws on average than the best acceptance-rejection policy so far, as this already needs three throws for the first round.

4. An optimal strategy

In the above we learned that when the class contains \(N=19\) children, the monkey strategy, which prescribes to continue throwing the die until a winning pattern emerges, does not give a fair outcome. Hence, for the moment, by 1 the best fair algorithm for \(N=19\) seems to throw three times: if that gives a winner, stop; otherwise throw three times again, and continue with rounds until there is a winner.

However, there is a strategy that is fair and minimizes the expected number of throws at the same time. This strategy we discuss now. Although a bit harder to understand, it's an elegant interplay between probability and number theory.

In the sequel we write \(Z\sim\Unif{\{0, 1, \ldots, n\}}\) to say that a rv \(Z\) is uniformly distributed on the set \(\{0, 1, \ldots, n\}\). We also write \(Z|A\) to denote a rv \(Z\) given that the event \(A\) materialized.

Let us first estimate the number of throws needed for a class with \(N=20\) children; this is somewhat simpler than the case with \(N=19\). Consider the following code, where \(X\) is an iid uniform rv on \(\{0,\ldots,5\}\) corresponding to the throw of the die. The line t % 20 computes the remainder of t after dividing by \(20\).

import numpy as np

rng = np.random.default_rng(1)

t = 0
while t < 16:
    X = rng.integers(6)
    t = 6 * t + X
t = t % 20

Explain that \(t_{1}\sim \Unif{\{0, \ldots, 5\}}\), \(t_{2}\sim \Unif{\{0, \ldots, 35\}}\), and \(t_{i}\sim \Unif{\{16,\ldots 95\}}\) for any throw \(i\geq 3\). Then explain why \(t\sim \Unif{\{0, 1, \ldots, 19\}}\) when the algorithm terminates.

Solution

We start with \(t_{0}=0\). After the first throw

\begin{equation*} t_{1} = 6 t_{0} + X_{1} = X_{1} \sim \Unif{\{0, \ldots, 5\}}. \end{equation*}

After the second, the rv \(6t_{1} \sim \Unif{\{0, 6, 12, 18, 24, 30\}}\). And thus, \(t_{2} = 6t_{1} + X_{2} \sim \Unif{\{0, 1, 2, \ldots, 35\}}\). If \(t_{2} \geq 16\), then \(t_{2}\in \{16, 35\}\). But in this case \(t_{2} | t_{2} \geq 16\) is uniform on a set with 20 elements. Thus, by taking the remainder \(t_{2} \pmod{20}\) we get a uniform rv on \(\{0, 1, 2, \ldots, 19\}\). If, on the other hand, \(t_{2} < 16\), then \(t_{2} | t_{2} < 16 \sim \Unif{\{0, 1, \ldots, 15\}}\). A third throw then gives \(t_{3} = 6t_{2}+ X_{2} \sim\Unif{\{0, 1, 2, \ldots, 95\}}\). When \(t_{3}\in \{16, 95\}\) we can divide by 20 and take the remainder (observe that the set \(\{16, 17,\ldots, 95\}\) has 80 elements). And, when \(t_{3}\leq 15\), throw again, and so on.


Why does this algorithm minimize the expected number of throws to obtain a rv uniform on \(\{0, 1, \ldots, 19\}\)?

Solution

After every throw, the set that requires another throw is minimal. Specifically, of the set \(\{0, 1, \ldots, 95\}\) we can at most remove 80 elements to guarantee a uniform drawing after dividing by 20 and taking the remainder.

The above two exercises explain the threshold \(16\) of the algorithm for \(N=20\). Now that we understand this, we can try to generalize the algorithm to other values for \(N\), such \(N=19\). Consider the following update rule for \(i=1, 2, \ldots\), \(t_0=0\), and \(X_1\sim\Unif{\{0, \ldots, 5\}}\),

\begin{equation*} t_i = 6 t_{i-1} + X_{i}. \end{equation*}

Take \(N=19\). After the second throw, \(t_{2}\sim \Unif{\{0, 1, \ldots, 35\}}\). In view of the above, what subset of \(\{0, 1, \ldots, 35\}\) can we use as an acceptance set to generate a rv \(\sim \Unif{\{0, \ldots, 18\}}\) from \(t_{2}\)? What should be the rejection set for \(t_{2}\)? Finally, why should we replace the threshold \(16\) for the case \(N=20\) by \(17\) for \(N=19\)?

Solution

We don't have to rethrow when \(t_{2}\in \{17, 18, \ldots, 35\}\), as this set contains \(35 - 16 = 19\) elements. However, when \(t_{2}\) lies in \(\{0, 1, \ldots, 16\}\), we need to throw again. Thus, the threshold for \(t_{2}\) should be \(17\) rather than \(16\).


For \(N=19\), we should change the acceptance threshold to 7 after the third throw. Why?

Solution

As \(t_2\) is maximally \(16\), the largest value of \(t_{3}\) is \(6 \times 16 + 5 = 101\). To make the probability of a rethrow as small as possible, we take \(7\) as a threshold since \(101 - 5\times 19 = 6\). The event \(\{7, 8, \ldots, 101\}\) contains \(95 = 5\times 19\) elements, so on this subset we can divide by \(19\) and take the remainder to get a uniform rv on \(\{0, 1, \ldots, 18\}\).

Notice that for \(N=19\) the threshold is not the same for each throw, i.e., 17 for the second and 7 for the third throw.

Explain again why for \(N=20\) the threshold remains \(15\).

Solution

\(N=20\) is a special case. We have seen earlier that \(t_2\) is maximally \(15\) in this case. But then, \(t_{3} = 6 \times 15 = 95\). Removing the largest possible acceptance set containing \(4\times 20 = 80\), we see that \(6\times 15 + 5 - 4 \times 20 = 15\). Thus, the thresold \(15\) is a fixed point.

Clearly, to deal with cases such as \(N=19\) we need to modify the above algorithm. Here is a generalized version.6

def select_uniformly_with_a_die(N):
    n = r = 1
    t = 0
    while t < r:
        X = rng.integers(6)
        t = 6 * t + X
        n = 6 * r
        r = n % N
    return t % N

What sequence of (n,r) does select_uniformly_with_a_die produce for the cases \(N=2\), \(N=3\), and \(N=6^{3}=216\) (or any other power of 6)? When does it terminate?

Solution

For \(N=2\), starting with n = r = 1, t = 0, after one throw (n,r) = (6,0), hence \(t_{1} = 6t_{0}+X = 6\cdot 0 + X \geq 0 = r\). Thus, the algo terminates, and the winning child is \(t_{1}= X \pmod{2} \in \{0, 1\}\). For \(N=3\) it works the same: we have that \(t=X \pmod{3} \in \{0, 1, 2\}\). For \(N=6^3=216\), we see that (n, r) progresses as \((1,1) \to (6,6) \to (36, 36) \to (216, 0)\). Since now \(r=0\), the algorithm necessarily terminates after three steps.


The same questions, but now for \(N=4\). With this, show that \(\E Y = 4/3\).

Solution

For \(N=4\), (n, r) develops as \((1,1) \to (6, 2) \to (12, 0)\). As after the first throw \((n,r)=(6,2)\), the probability to accept the outcome is \((n-r)/n = 4/6\), and to reject is \(r/n = 2/6\). After the second throw \((n,r)=(12,0)\), so any outcome will be accepted, and the algorithm terminates Hence, the expected number of throws is \(E Y =\frac{4}{6} + 2\cdot \frac{2}{6} = \frac{4}{3}\).


Explain that \(\E Y = 72/35\) for \(N=7\). Compare with 1.

Solution

For \(N=7\), the tuple (n, r) progresses as \((1,1) \to (6, 6) \to (36, 1)\to (6, 6) \to \) and so on. Hence,

\begin{equation} \E Y= 1 + 1 + \frac{1}{36}\left(1 + 1 + \frac{1}{36}( 1 + 1 \ldots)\right). \end{equation}

In other words,

\begin{equation*} \E Y = 2 + \frac{1}{36}\E Y \implies \frac{35}{36}\E Y = 2 \implies \E Y = 72/35. \end{equation*}

Why does this algorithm work for general \(N\)?

Solution

The variable \(r\) contains the remainder of \(n\) after dividing by \(N\). The largest that \(t\) can become after a new iteration is \(6r + 5\), while the smallest value of \(t\) is \(0\), which we get after successive outcomes of \(X=0\). Thus, \(t\in \{0, 1, \ldots 6r + 5\}\). This set contains \(n=6r\) elements. By taking \(r\) to \(n \pmod{N}\) we minimize the size of the rejection set for the next throw.

Let us test this generalized algorithm numerically. Here is the code.

count = collections.Counter()
for _ in range(num_runs):
    s = select_uniformly_with_a_die(19)
    count[s] += 1

most = count.most_common()[0][1]
least = count.most_common()[-1][1]
mean = num_runs // 19

print(f"{len(count)=}, {count.total()=}")
print(f"{least=}, {mean=}, {most=}")
len(count)=19, count.total()=100000
least=5144, mean=5263, most=5434

Think about the simplest possible test of the correctness of this algorithm. Does the output satisfy this test?

Solution

We see that at least all children can become a winner because count has 19 elements..

If we compare this output to our earlier results, the difference between the least and most winning child is reasonable. In fact, comparing the occurrences, we see that our algorithm performs very well!

The last step is to estimate the expected number of throws \(\E Y\) required for the case \(N=19\). First, however, we consider \(N=20\) because in this case we can obtain a simple closed-form expression for \(\E Y\).

For \(N=20\) explain that

\begin{align*} \E Y &= 2 + \frac{16}{36} \E Z, & \E Z &= 1 + \frac{16}{96} \E Z. \end{align*}

Hence \(\E Z = 6/5\) and \(\E Y = 2.5333\).

Solution

We need at least 2 throws. When \(t < 16\), which happens with probability \(16/36\), we need to rethrow. The rv \(Z\) is the number of throws, if we need to rethrow. Since \((r, n)=(16, 96)\),

\begin{equation*} \E Z = 1 + \frac{16}{96} \E Z \implies \E Z = 1 + \frac{1}{6}\E Z \implies \E Z = \frac{6}{5}. \end{equation*}

To compute \(\E Y\) for general \(N\) we can slightly modify the above algorithm.

Explain the algorithm below.

Solution

The overall algorithm specifies to continue after iteration \(i\) when \(t < r_{i}\), and otherwise to stop. The probability per iteration to continue is \(r_{i}/n_{i}\). Thus, the probality to survive for \(i\) rounds is \(\alpha_i = \alpha_{i-1} r_i/n_i\). Adding up these probabilities gives us \(\E Y\).

Numerically we stop when \(\alpha_i \leq \epsilon\), with \(\epsilon\) some small number.

N = 20
n = r = 1
alpha, eps = 1, 1e-3
EY = 0
while alpha >= eps:
    EY += alpha
    n = 6 * r
    r = n % N
    alpha *= r / n
    print(f"{n=:3d}, {r=:3d}, {r/n=:2.4f}, {alpha=:2.4f}, {EY=:2.4f}")
n=  6, r=  6, r/n=1.0000, alpha=1.0000, EY=1.0000
n= 36, r= 16, r/n=0.4444, alpha=0.4444, EY=2.0000
n= 96, r= 16, r/n=0.1667, alpha=0.0741, EY=2.4444
n= 96, r= 16, r/n=0.1667, alpha=0.0123, EY=2.5185
n= 96, r= 16, r/n=0.1667, alpha=0.0021, EY=2.5309
n= 96, r= 16, r/n=0.1667, alpha=0.0003, EY=2.5329

This seems to converges to \(2.533\). The last task is to run the code for \(N=19\). This gives \(\E Y=2.509\). Comparing this to the monkey strategy of 1 we see that this algorithm does better on all respects: it is fair and requires less throws on average.

And now two final questions.

What is the relation between this good algorithm and the monkey-typing strategy, and what is the major difference?

Solution

Both policies start with throwing twice, and then continue with single throws to decide to stop or not. So, both policies don't work with rounds of throws. The difference is in the acceptance set. In particular, for the good policy, this set is dynamic, i.e., depends on the value of \((n,r)\).


Can it ever happen for \(N=19\) and a \(6\) sided die that the rejection set becomes empty? Recall, when \(N=4\), the rejection set is empty after two steps, since \(r_2=0\).

Solution

No, this is impossible. Take any \(r\in \{0,1,\ldots, 17\}\), and set \(n=6r\). Then \(n\) and \(19\) are co-prime, in other words, the greatest common divisor of \(n\) and \(19\) is \(1\). This implies that \(r = n \pmod 19 \in \{0, 1,\ldots, 17\}\).

5. Summary

By studying the above toy problem we covered many nice and useful ideas. We developed acceptance-rejection strategies to select a winner. Then we tried to speed up this algorithm to the monkey algorithm, but this algorithm turned out to be unfair. However, by improving the idea, we were able to find a fair and optimal algorithm. This final algorithm is not quite trivial, but effective and suprisingly short.

In the next section we'll look at the monkey-typing strategy. This will provide us with a number of useful tools, in particular first-step analysis.

Footnotes:

1

It seems like an experiment in which the aim is to prove that a new medicine works. If the data shows that the medicine does not work, just repeat.

2

I frequently mess up these rvs.

3

There is subtle detail for this idea to work, \(\E Y\) should be finite. We will ignore such mathematical points in the sequel.

4

Ask ChatGPT to explain code you don't understand.

5

Finding good objectives is most often not a mathematical problem, in fact, some (or much) wisdom is needed to understand when to use mathematics, and when to stop using mathematics.

6

We assign the outcome of the throw of the die to the variable \(X\) just for clarity.

Making an Exercise and Solution Environment

1. The Problem

In a number of my org mode files I include exercise with hints and solutions. In the pdf files, which I make via \(\LaTeX\), I use the answers package print the exercises in the main text and the hints and solutions at the end of the file. However, when exporting the file to html via my website I would like to see the exercises, hints and solutions printed like so:

How to make an excercise environment like this?

Hint

And a hint like this?

Solution

To see how to do this, read on.

2. My Environment

I use emacs and org mode to write files, and nikola to make my homepage. After some searching on the web, I found the org-special-block-extras package which proved promissing for my goal. The related github page shows how to install it. Here are the steps I followed.

First I installed the org mode plugin for nikola. The orgmode.py file shows that the next command can used to convert an org to an html file that nikola can read.

/usr/bin/emacs --batch -l init.el --eval '(nikola-html-export "input.org" "output.html")'

The org mode plugin also provides an init.el file that I used as a starting point.

The init.el file contains the line (setq package-load-list '((htmlize t))). This should be removed. Why?

Hint

What does package-load-list do?

Solution

I need to install org-special-block-extras, and this command prevents that.

3. The adapted init.el

Since I need functionality from org-special-block-extras, I have to update the init.el anyway, so here it is.

3.1. Loading the relevant packages

Since I use straight and use-package, I need to put this on the top of the init.el file. I copied this straight from my regular init.el

(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 5))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))
(setq package-enable-at-startup nil)

(straight-use-package 'use-package)
(setq straight-use-package-by-default t)

Next I load two packages. The hook on org-mode is necessary.

(use-package htmlize
  :ensure t)

(use-package org-special-block-extras
  :ensure t
  ;; All relevant Lisp functions are prefixed ‘o-’; e.g., `o-docs-insert'.
  :hook (org-mode . org-special-block-extras-mode)
  :custom
    (o-docs-libraries
     '("~/org-special-block-extras/documentation.org")
     "The places where I keep my ‘#+documentation’"))



(require 'org)
(require 'ox-html)
(require 'org-special-block-extras)

3.2. Updating the code for box and details blocks

I adapted the fontsizes and the padding of the details block of org-special-block-extras. I just copied the relevant part of org-special-block-extras.el, removed the documentation string to keep this file small, and changed the padding and font size.

(org-defblock details (title "Details"
              background-color "#e5f5e5" title-color "green")
   (pcase backend
     (`latex (concat (pcase (substring background-color 0 1)
                       ("#" (format "\\definecolor{osbe-bg}{HTML}{%s}" (substring background-color 1)))
                       (_ (format "\\colorlet{osbe-bg}{%s}" background-color)))
                     (pcase (substring title-color 0 1)
                       ("#" (format "\\definecolor{osbe-fg}{HTML}{%s}" (substring title-color 1)))
                       (_ (format "\\colorlet{osbe-fg}{%s}" title-color)))
                     (format "\\begin{quote}
                              \\begin{tcolorbox}[colback=osbe-bg,colframe=osbe-fg,title={%s},sharp corners,boxrule=0.4pt]
                                   %s
                               \\end{tcolorbox}
                \\end{quote}" title contents)))
     (_ (format "<details class=\"code-details\"
                 style =\"padding: 0.6em;
                          background-color: %s;
                          border-radius: 15px;
                          color: hsl(157 75% 20%);
                          font-size: 1em;
                          box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;\">
                  <summary>
                    <strong>
                      <font face=\"Courier\" size=\"3\" color=\"%s\">
                         %s
                      </font>
                    </strong>
                  </summary>
                  %s
               </details>" background-color title-color title contents))))

And also for the box block.

(org-defblock box (title "" background-color nil shadow nil)
  (pcase backend
    (`latex
     (apply #'concat
            `("\\begin{tcolorbox}[title={" ,title "}"
              ",colback=" ,(pp-to-string (or background-color 'red!5!white))
              ",colframe=red!75!black, colbacktitle=yellow!50!red"
              ",coltitle=red!25!black, fonttitle=\\bfseries,"
              "subtitle style={boxrule=0.4pt, colback=yellow!50!red!25!white}]"
              ,contents
              "\\end{tcolorbox}")))
    ;; CSS syntax: “box-shadow: specification, specification, ...”
    ;; where a specification is of the shape “[inset] x_offset y_offset [blur [spread]] color”.
    (_ (-let [haze (lambda (left right deep-right deep-left)
                     (format "width: 50%%; margin: auto; box-shadow: %s"
                             (thread-last (list (cons right      "8px 6px 13px 8px %s")
                                                (cons left       "-16px 12px 20px 16px %s")
                                                (cons deep-right "48px 36px 71px 28px %s")
                                                (cons deep-left  "-48px -20px 71px 28px %s"))
                               (--filter (car it))
                               (--map (format (cdr it) (car it)))
                               (s-join ","))))]
         (format "<div style=\"%s\"> <h3>%s</h3> %s </div>"
                 (s-join ";" `( "padding: 0.5em;"
                               ,(format "background-color: %s" (org-subtle-colors (format "%s" (or background-color "green"))))
                               "border-radius: 15px"
                               "font-size: 1em"
                               ,(when shadow
                                  (cond
                                   ((equal shadow t)
                                    (funcall haze "hsl(60, 100%, 50%)" "hsl(1, 100%, 50%)" "hsl(180, 100%, 50%)" nil))
                                   ((equal shadow 'inset)
                                    (funcall haze "inset hsl(60, 100%, 50%)" "inset hsl(1, 100%, 50%)" "inset hsl(180, 100%, 50%)" nil))
                                   ((or (stringp shadow) (symbolp shadow))
                                    (format "box-shadow: 10px 10px 20px 0px %s; width: 50%%; margin: auto" (pp-to-string shadow)))
                                   ((json-plist-p shadow)
                                    (-let [(&plist :left X :right Y :deep-right Z :deep-left W) shadow]
                                      (funcall haze X Y Z W)))
                                   (:otherwise (-let [(X Y Z W) shadow]
                                        (funcall haze X Y Z W)))))))
                 title contents)))))

3.3. Making the exercise, hint and solution blocks

The exercise, hint and solution blocks are now simple to implement (after I read the examples on org-special-blocks-extra).

(org-defblock exercise (title nil)
  (org-thread-blockcall raw-contents
    (box )))

(org-defblock solution (title "Solution")
  (org-thread-blockcall raw-contents
    (details title :title-color "red")))


(org-defblock hint (title "Hint")
  (org-thread-blockcall raw-contents
    (details title))) ; :title-color "red")))

I copied the above on top of the init.el file, and kept the rest.

4. Testing

Let's test this on this file. As this file sits in the posts we need to include the path to init.el.

/usr/bin/emacs --batch -l ../plugins/orgmode/init.el --eval '(nikola-html-export "making-an-exercise-environment.org" "../output/output.html")'

Snell's Envelope, House Selling and Buying

1. Problem Setting

I have a house for sale and I am prepared to organize at most \(n\geq 1\) house viewings. Assume that offers are idd rvs \(\sim \Unif{[0,1]}\), and that I have to accept an offer on the spot or reject it. If rejected, the offer is retracted, hence I cannot accept it at a later date. Under these rules, what is the best price I can expect?

Once my house is sold, I want to buy a new house. As this is a multi-objective optimization problem, I don't want to use just the price of the house to decide to buy it or not. In this case I just assume I can rank the houses from best to worse. Thus, I cannot use the same policy as when selling a house, but need some other policy to decide.

Both these problems can be solved as an optimal stopping problem. So, let's see how to do this, partly with Snell's envelope. I include some python code to get numerical output.

2. Selling a House

When selling a house, at most \(n\) buyers will present their offers \(\{X_i\}_{i=1}^n\) , iid and uniform on \([0,1]\), one after the other. Rejected offers are lost. What is the best price? I used this site, where the same problem is discussed in terms of the Secretary Problem.

2.1. Best Price Under a Fixed Threshold Policy

Let's start with a simple policy: I accept the first offer that exceeds some threshold \(q\) that remains fixed for the entire sales period. This threshold policy can be formulated in terms of the stopping time \(\tau = \inf\{i : X_i \geq q\}\), where \(\inf \varnothing = \infty\) so that \(\tau=\infty\) when all \(X_i < q\). The expected sales price becomes

\begin{equation}\label{snell-eq1} \E{X_{\tau}} = \sum_{i=1}^n \E{X_i\1{\tau = i}} + \E{X_n \1{\tau=\infty}}, \end{equation}

because the event \(\{\tau = i \}\) implies that the offer \(X_i\) of buyer \(i\) exceeds \(q\), and when \(\tau=\infty\) the threshold \(q\) was too high so that I have to accept the offer \(X_n\) of the last buyer.

Why is \(\E{X_i \1{\tau=i}} = q^{i-1} \frac{1-q^{2}}{2}\) for \(i < n\)?

Solution

By independence, for \(i\leq n-1\),

\begin{align*} \E{X_i \1{\tau=i}} &= \E{X_i \1{X_i \geq q} \prod_{k=1}^{i-1} \1{X_k < q}} = q^{i-1} \E{X_i \1{X_i \geq q}} \\ &= q^{i-1} \int_q^1 x \d x = q^{i-1} \frac{1-q^{2}}{2}, \end{align*}

Why is \(\E{X_n \1{\tau=\infty}}= \frac{q^{n+1}}2\) for \(n\)?

Solution \begin{align*} \E{X_n \1{\tau=\infty}} &= \E{X_n \1{X_i < q} \prod_{k=1}^{n-1} \1{X_k < q}} = q^{n-1} \int_0^q x \d x = \frac{q^{n+1}}2. \end{align*}

Conclude from 2.1 that:

\begin{align*} v_{n}(q) := \E{X_{\tau}} &= \frac{1}{2}(1-q^{n} + q). \end{align*}
Solution \begin{align*} \E{X_{\tau}} &= \frac{1-q^{2}}{2} \sum_{i=0}^{n-1} q^{i} + \frac{q^{n+1}}2 = \frac{1+q}{2} (1-q^{n}) + \frac{q^{n+1}}2 \\ &= \frac{1}{2}(1-q^{n} + q) =: v_n(q). \end{align*}

Show that for given \(n\) the optimal \(q\) is equal to:

\begin{align*} q = \left({\frac{1}{n}}\right)^{\frac{1}{n-1}}. \end{align*}
Solution

Solve for \(q\) in \(\d v_n(q)/ \d q = -n q^{n-1} + 1 = 0\).

Here is an example in code to see how to implement all this.

def best_q(n):
    return (1 / n) ** (1 / (n - 1))


def v(n, q):
    res = 1 - q**n + q
    return res / 2


n = 10
q = best_q(n)
value = v(n, q)
print(f"{q=}")
print(f"{v(n, q)=}")
q=0.7742636826811271
v(n, q)=0.8484186572065071

For the future I might consider some additional questions.

  1. What is the probability that I will obtain the best price?
  2. What is the marginal value of increasing the number of house viewings by one?
  3. Suppose each viewing costs \(c\in [0, 1]\), what is the optimal number of house viewings?

2.2. Best Price Under a Dynamic Threshold Policy

A fixed threshold policy cannot be optimal, because the less visits we have left, the lower the acceptance threshold should be. We next find the optimal levels when we have \(1 \leq k \leq n\) visits to go.

Suppose we have rejected the first \(n-1\) offers, so that we have only \(k=1\) visits to go. Then our expected price is \(v_1 = \E{X_n} = 1/2\). In general we accept offer \(X_{i}\) of buyer \(i\) when this exceeds the expected price \(v_{k}\) of the remaining \(k=n-i\) visits. Otherwise, we reject this offer. In other words, the expected price for \(k\) periods to go satisfies the recursion

\begin{align*} v_k &:= \E{\max{X_{n-k}, v_{k-1}}} = \int_{v_{k-1}}^1 x \d x + \int_0^{v_{k-1}} v_{k-1} \d x = \frac{1}{2}(1-v_{k-1}^2) + v_{k-1}^2 \\ &= \frac{1}{2}(1 + v_{k-1}^2). \end{align*}

Note that \(v_{k} > v_{k-1}\) when \(v_{k-1}\neq 1\) because \(1 + v_{k-1}^2 > 2 v_{k-1} \iff v_{k-1} \neq 1\). Thus, a policy with a fixed threshold is not optimal.

With caching/memoization the code is very simple.

from functools import cache


@cache
def v(k):
    if k == 1:
        return 1 / 2
    return (1 + v(k - 1) ** 2) / 2


print(f"{v(10)=}")
v(10)=0.861098212205712

2.3. Snell's Envelope

The optimal policy can be found by using a more general, but more abstract, framework, namely by means of Snell's envelope. Perhaps the easiest way to see how this works is to start with a theorem, stripped from some of its technical clutter. I copied it from `Promenade alétoire' by M. Benaïm and N. El Karoui.

Consider a sequence of rvs \(\{X_i\}_{i=1}^n\) adapted to a filtration \(\{\F_{i}\}_{i=1}^{n}\). Introduce the sequence of rvs \(\{Y_i\}\) by backward recurrence as

\begin{align*} Y_i &= \sup\{\E{Y_{i+1}|\F_{i}}, X_{i}\}, \quad i < n, & Y_n &= X_{n}, \end{align*}

and the stopping time \(\tau^*= \inf\{ i\leq n: Y_i = X_i\}\). Then,

  1. the sequence \(\{Y_i\}\) is a super martingale, i.e., \(Y_i \geq \E{Y_{i+1} | \F_{i}}\) a.s.;
  2. \(\E{Y_1} = \E{Y_{\tau^*}} = \E{X_{\tau^*}} = \sup_{\tau\in T} \E{Y_\tau}\), where \(T\) is the set of stopping times smaller or equal to \(n\);
  3. the stopping time \(\tau^{*}\) is optimal.

The rv \(Y_i\) is the Snell envelope of \(X_i\), which is defined as the smallest super martingale \(\geq X_i\).

Let's apply this theorem to the house selling problem. Take \(\F_{i} = \sigma\{X_k : k \leq i\}\). Starting the recursion from right to left,

\begin{align*} \E{Y_{n}} &= \E{X_{n}} = v_{1}, \end{align*}

since just before period \(n\), we have \(k=1\) buyers to go. From this, using independence,

\begin{align*} \E{Y_{n} |\F_{n-1}} &= \E{X_{n}|\F_{n-1}} = \E{X_{n}} = v_{1}, \\ Y_{n-1} &= \max{\E{Y_{n} |\F_{n-1}}, X_{n-1}} = \max{v_1, X_{n-1}}, \\ \E{Y_{n-1}|\F_{n-2}} &= \E{\max{v_1, X_{n-1}}} = v_{2}, \end{align*}

as just before buyer \(n-1\) makes an offer, we have \(2\) offers to go. Continuing like this we retrieve our earlier values \(v_k\). Morever, when \(Y_i = X_i\) for the first time \(i\), then \(X_i \leq v_{n-i}\), that is, \(X_i\) is the first offer that exceeds the expected price when continuing. But this is precisely the policy we found above. By the theorem the stopping time \(\tau^*\) is optimal among all sellers (policies) that are willing to have \(n\) house visits.

3. Buying a House

Contrary to when selling a house, when buying a house I don't have an absolute benchmark. In the house selling case, all offers are capped by \(1\), and when the very first bid is already very close to \(1\), I know that that the probability of getting a better offer at a later date will be small. However, when buying a house, I assume I can just rank the houses, but I don't have benchmark. Therefore I will use the first \(r\) say visits out of maximally \(n\) to sample the quality of the houses.

To understand how to apply Snell's envelope to the house selection problem, I used these two resources besides `Promenade aléatoire' by M. Benaïm and N. El Karoui.

These accounts are clear, but leave quite a bit of work to the reader. The idea here is to also do this work.

We assume that \(n > 2\), because when \(n=1,2\) the problem is not interesting.

3.1. A heuristic procedure

A simple policy to select a house is like this: choose a level \(r>1\), reject the first \(r-1\) houses visited, then continue sampling and choose house \(k \geq r\) when house \(k\) is the best so far. Thus, the first \(r-1\) visits form a sampling set and the threshold is the best of the houses in this sampling set. The problem is to find the threshold \(r\) that maximizes the probability of selecting the best house of all \(n\) houses. As it turns out, this is the also the best policy overall. Before developing notation and using Snell's envelope to prove the optimality, we discuss this policy in heuristic terms.

We approach the problem in steps. Suppose we would choose house \(k\) no matter what. Then, clearly, for any \(k\),

\begin{align*} \P{\text{House $k$ is best}} = \frac{1}{n}. \end{align*}

So, let us follow the policy to reject the first \(r-1\) houses. Then,

\begin{align*} \P{\text{Select house $k$, given that house $k$ is best}} = \frac{r-1}{k-1}. \end{align*}

Explain this result.

Solution

Under our simple policy, to select house \(k\) it is necessary that house \(k\) is better than all earlier houses, and that all houses \(r, r+1, \ldots, k-1\) are worse than the best house in \(1, 2, \ldots r-1\). Since all sequences of houses are equaly likely, the probability that this second best house occurs in the first \(r-1\) visits of the \(k-1\) visits before house \(k\), is \((r-1)/(k-1)\).

The best house can be visited at any visit from \(r, r+1, \ldots, n\). Thus, using the above,

\begin{align*} P(r)&:=\P{\text{Select the best house}} \\ &= \sum_{k=r}^{n} \P{\text{Select the best house, house $k$ is best}} \\ &= \sum_{k=r}^{n} \P{\text{Select the best house, given house $k$ is best}} \P{\text{House $k$ is best}} \\ &= \frac{r-1}{n}\sum_{k=r}^{n} \frac{1}{k-1}. \end{align*}

The best sampling level \(r\) maximizes \(P(r)\). Now, heuristically, \(P(r)\) must first increase as a function of \(r\), and then decrease. When \(r\) is small, the threshold to accept a house is too low because we did not sample enough houses, while if \(r\) is large, it is likely that the best house will rejected because it will lie in the sampling set. So, we have to look for the \(r\) after which \(P(r)\) starts to decrease.

Show that \(P(r) \geq P(r+1) \iff 1 \geq \sum_{k=r}^n \frac{1}{k-1}\).

Solution \begin{align*} P(r) \geq P(r+1) &\iff \frac{r-1}{n} \sum_{k=r}^n \frac{1}{k-1} \geq \frac{r}{n} \sum_{k=r+1}^n \frac{1}{k-1} \\ &\iff r\left( \sum_{k=r}^n \frac{1}{k-1} - \sum_{k=r+1}^n \frac{1}{k-1} \right) \geq \sum_{k=r}^n \frac{1}{k-1} \\ &\iff \frac{r}{r-1} \geq \frac{1}{r-1} + \sum_{k=r+1}^n \frac{1}{k-1}\\ &\iff 1 \geq \sum_{k=r+1}^n \frac{1}{k-1}. \end{align*}

Consequently, the best \(r\) is given by

\begin{equation} \label{org636acc8} r^{*} = \min{r > 1: P(r) > P(r+1) } = \min {r>1 : \sum_{k=r}^n \frac{1}{k-1} \geq 1}. \end{equation}

Note that for \(n > 2\), the policy with \(r=1\) can be ruled out right away as not optimal.

3.2. The formal procedure

We formalize the problem specification so that we can apply Snell's envelope to prove that the sampling policy with threshold \eqref{org636acc8}just developed is optimal.

If we have seen all houses, we can rank them so that \(R_i\) is the rank of the \(i\)th house visited. We say that house \(i\) is better than house \(j\) when \(R_i < R_j\); the best house overall has rank \(1\). The problem, once again, is to maximize the probability to find the best house.

Write \(\xi_i = 1\) if house \(i\) is the highest ranked so far and \(\xi_i=0\) otherwise, that is,

\begin{align*} \xi_i := \1{R_i < \min{R_{1}, \ldots, R_{i-1}}}. \end{align*}

All permutation of the rankings are equally likely, implying that,

  1. \(\P{\xi_i = 1} = 1/i\) for \(i=1, \ldots n\),
  2. The rvs \(\{\xi_i\}\) are independent.

Let \(\F_{i}\) be \(\sigma\{\xi_1, \ldots, \xi_i\}\), and \(\{\F_{i}\}_{i=0}^{n}\) the associated filtration.

Prove properties 1 and 2.

Solution

For 1, observe that \(\{\xi_i=1\}\) imposes the condition that of all permutations only the ones are allowed such that the best ranking of the first \(i\) visits appears at visit \(i\). By symmetry, the probability that visit \(i\) is the best of all \(i\) visits is equal to \(1/i\). After proving 2, we provide another proof.

For 2, suppose that \(j>i\). By 1, the probability that \(\xi_j = 1\) is \(1/j\). Next, consider the subset of permutations such that \(\xi_j=1\). All of these permutations have the same probability. Thus, the probability that one of these permutations is such that \(\xi_i=1\) is \(1/i\). Therefore \(\P{\xi_i=\xi_j=1} = \frac{1}{ij}\). By recursion, this argument applies to any number of \(\xi_k\).

Here is a second proof for property 1, by means of an example. Suppose we do \(26\) visits, and rank the quality from \(a\) (lowest) to \(z\) (highest). The number of permutations of the 26 letters of the alphabet such that the letter \(e\) appears at the fourth position and is the best so far is \(4\cdot 3\cdot 2 \cdot (26-4)!\), because for the first position we can choose one letter from the set \(\{a, b, c, d\}\), for the second position we have 3 letters left of this set, for the third position just 2 letters, and after the fourth position (occupied by the \(e\)) any permutation of the remaining \(26-4\) is allowed. More generally, the number of permutations such that best of four appears at the fourth place is

\begin{align*} (n-4)! \sum_{i=4}^n (i-1)(i-2)(i-3), \end{align*}

with \(n=26\). The probability of the event \(\{\xi_4=1\}\) is therefore

\begin{align*} \P{\xi_4=1} \ &= \frac{(n-4)!}{n!} \sum_{i=4}^n (i-1)(i-2)(i-3) \\ &= \frac{3!(n-4)!}{n!}\, \sum_{i=3}^{n-1} \frac{i(i-1)(i-2)}{3!} \\ &= \frac{1}{4} \frac{1}{{n \choose 4}} \sum_{i=3}^{n-1} {i \choose 3}. \end{align*}

With induction it is simple to prove that \(\sum_{i=3}^{n-1} {i \choose 3} = {n \choose 4}\). It is also evident that the number \(4\) places no special role, so property 1 follows.

Suppose we would stop according to stopping rule \(\tau\), the probability we have the best house is given by

\begin{align*} \E{\1{R_{\tau}=1}} = \P{R_{\tau} = 1} = \P{\xi_{\tau} = 1, \xi_{\tau+1} = \cdots = \xi_n = 0}. \end{align*}

There is a fundamental problem with the event \(\{\xi_{i} = 1, \xi_{i+1} = \cdots = \xi_n = 0\}\) because this is not an element of \(\F_{i}\) since \(\xi_{j} \not \in \F_{i}\) for \(j> i\). Let us therefore look instead at the rvs that are adapted to \(\{F_i\}\),

\begin{align*} X_i &= \E{\1{R_i=1}|\F_{i}} \\ &= \P{\xi_{i} = 1, \xi_{i+1} = \cdots = \xi_n = 0|\F_i} \\ &=\xi_{i} \P{\xi_{i+1} = \cdots = \xi_n = 0|\F_i}, \\ &=\xi_{i} \frac{i}{i+1} \times \cdots \times \frac{n-1}{n} = \xi_i \frac{i}{n}. \end{align*}

With regard to the Snell envelope of \(X_n\), and using independence,

\begin{align*} Y_n &= X_n = \xi_n \frac{n}{n}, & \E{Y_n | \F_{n-1}} &= \E{X_{n}} = \E{\xi_{n}} = \frac{1}{n}. \end{align*}

To see whether we can spot a pattern, consider next \(X_{n-1}\). By independence and the above,

\begin{align*} Y_{n-1} &= \max{X_{n-1}, \E{Y_{n}|\F_{n-1}}} \\ &= \max{\xi_{n-1}\frac{n-1}{n}, \frac{1}{n}} = \frac{1}{n} + \frac{n-2}{n} \xi_{n-1}, \end{align*}

since \(n-1 > 1\) (recall \(n\geq 2\) by assumption). Therefore,

\begin{align*} \E{Y_{n-1}|\F_{n-2}} &= \E{Y_{n-1}} = \frac{1}{n} + \frac{n-2}{n}\E{\xi_{n-1}} \\ &=\frac{n-2}{n}\frac{1}{n-2} + \frac{n-2}{n} \frac{1}{n-1} \\ &= \frac{n-2}{n} \sum_{k=n-1}^n \frac{1}{k-1}. \end{align*}

Based on this, let's guess that for \(i\) sufficiently large,

\begin{align*} \E{Y_{i+1}|\F_{i}} = \E{Y_{i+1}} = \frac{i}{n} \sum_{k=i+1}^n \frac{1}{k-1}. \end{align*}

Then

\begin{align*} Y_{i} &= \max{X_{i}, \E{Y_{i+1}|\F_{i}}} = \max{\xi_i \frac{i}{n}, \E{Y_{i+1}|\F_{i}}} \\ &= \frac{i}{n}\max{\xi_i , \sum_{k=i+1}^n \frac{1}{k-1}}. \end{align*}

If \(i\) is in fact so large that \(1 \geq \sum_{k=i+1}^{n} \frac{1}{k-1}\),

\begin{align*} \E{Y_{i}|F_{i-1}} &= \E{Y_{i}} = \frac{i}{n}\left(\frac{1}{i} 1 + \frac{i-1}{i}\sum_{k=i+1}^n \frac{1}{k-1}\right) = \frac{i-1}{n} \sum_{k=i}^n \frac{1}{k-1}. \end{align*}

This validates, by induction, our earlier guess. However, if \(1 < \sum_{k=i+1}^{n} \frac{1}{k-1}\),

\begin{align*} Y_{i} &= \frac{i}{n}\max{\xi_i , \sum_{k=i+1}^n \frac{1}{k-1}} = \frac{i}{n}\sum_{k=i+1}^n \frac{1}{k-1}. \end{align*}

Use Snell's theorem to explain that the stopping time \eqref{org636acc8} is optimal.

Solution

Compute Snell's envelope with the formulas above. It is evident for all \(i\) that \(\xi_i = 0 \implies 0 = X_{i} < Y_i\). Next, starting from $i=1s, as long as \(i\) is such that \(1 < \sum_{k=i+1}^{n} \frac{1}{k-1}\), \(X_{i} < Y_i\) even when \(\xi_i = 1\). The earliest visit such that \(X_i = Y_i\) becomes possible is when \(\xi_i=1 > \sum_{k=i+1}^{n} \frac{1}{k-1}\). But this \(i\) is precisely the threshold given by \eqref{org636acc8}.

In conclusion, it is optimal to select house \(i\) when \(i\geq r^{*}\) and when \(\xi_{i} = 1\), i.e., when house \(i\) is the best house seen among the first \(i\) visits.

Vickrey auctions

1. Introduction

A Vickrey auction is a sealed-bid auction where bidders submit bids without knowing the bids of other people. The winner is the participant with the highest bid, but interestingly, the price paid is the second-highest bid price rather than the highest. One way to understand this is to think about an auction in which bidders accept a price announced by the auctioneer by raising their hand. If the second highest bidder is prepared to pay \(A\), then s/he will stop when the price is \(A+1\), and this is the price at which the highest bidder gets the item. Clearly, the highest bidder does not reveal its own maximum price to win the auction; in fact, all bidders except the highest reveal their bid.1

If we have large debts, and an expensive car, should we organize a Vickrey auction to sell our car and pay our debts? How will the expected value and the variance of the second bid depend on the number of bidders? Perhaps we need a minimum number of bidders to reduce the probability of getting a low value for our car. To obtain insight into these questions we will make a histogram of simulated prices at which the car is sold in a Vickrey auction.

As a simple model suppose we have \(10\) bidders whose bids are idd rvs \(\{X_i\}_{i=1}^{10}\) according to some distribution \(F\), say. To estimate the value of the second largest bid by simulation, we make a matrix \(X\) of size \(N\times 10\) with entries \(X_{i j} \sim F\); like this, each row corresponds to one round of an auction, and column \(j\) to the bids of the \(j\)th participant. Finding the hightest bid per row is easy with the code np.max(X, axis=1), but for the Vickrey auction we don't need the largest, but the second largest element of each row. One way to find this is to sort each row, and then pick the values of \(9\)th column of \(X\), i.e., X[:,8]. However, this is algorithmically more expensive than necessary: we don't need to sort all values of the row, we only need to the know the, so-called, \(k\)th median, with \(k=9\).

To build a better understanding of how to simulate efficiently the Vickrey auction, we first need to discuss some elementary, but highly useful, theory to express the complexity of algorithms. Then we apply this theory to an algorithm to solve the Traveling Salesman Problem (TSP) by means of recursion. As we will see, the implementation of the TSP is really elegant. Then we consider the complexity of two sorting algorithms. The second of these, quicksort, provides us with a very nice and efficient recursive algorith to find the \(k\)th median in an array of numbers. In the last section we use this algorithm to find the second highest bid in a Vickrey auction and plot of the density of these bids to obtain insight into the price we can expect for our car.

2. Some complexity theory

In simulation we often repeat the same algorithm with different so that we obtain statistical insight by means of many sample paths. Therefore the difference in time required by a dumb and a smart algorithm to complete one simulation run can be huge. One way to compare the speed of algorithms is by means of the concept of complexity, for which we use the order symbol \(O(\cdot)\). Here are two examples to illustrate the idea. First, when adding two \(n\)-digit numbers, we add the \(m\)th digit in the first number with the \(m\)th digit in the second number. We do the same operation for all digits, and since there are \(n\)-digits in total, the algorithm requires a number of additions that is \(O(n)\). Second, to multiply two \(n\)-digit numbers by hand, we multiply each digit of the first number by each digit of the second. Clearly this requires \(O(n^2)\) multiplications. Thus, with the \(O(\cdot)\) symbol, we express how the dominating term scales as a function of the input of the data, but we abstract away from the time each specific operation takes. In other words, whether an algorithm has \(4 n^3\) or \(5n^3\) complexity, in all such cases we say it is \(O(n^3)\). It is evident that the larger the power of the \(n\), the more execution time an algorithm need in general to complete for large input.

To practice a bit more with the \(O\) symbol, given a list of \(n\) elements, the following code finds the largest element:

X = [5, 8, 3]
M = X[0]
for x in X[1:]:
    M = max(M, x)

Explain that this algorithm requires \(O(n)\) comparisons.

Solution

We store the value of first element in a variable \(M\), say. Then we compare \(M\) to the second element, and replace the value of \(M\) by the second if the second value is larger than \(M\), and so on. Since we need \(n-1\) comparisons in total, the complexity is \(O(n)\).

A somewhat more interesting example is the complexity of the traveling salesman problem (TSP). This problem consists of finding the shortest possible route that visits, exactly once, each city of a given set of \(n\) cities, starting from a depot labeled \(0\). The next code compute recursively the length of the shortest path. We start at the depot and provide a set of cities. When in the current city, we solve the tsp that remains after going to each city in cities. (Take your time to understand the recursion.) We add typing to clarity what sort of argument TSP is supposed to accept. By the way, it might seem pointless to overload integers to city_id, but the meaning of the word city_id is much easier to understand.

city_id = int
length = float


def dist(i: city_id, j: city_id) -> length:
    # An arbitrary distance function between cities i and j
    return abs(i - j)


def tsp(current: city_id, cities: set[city_id]) -> length:
    if not cities:  # There are no remaining cities, so return home
        return dist(current, 0)
    L = []
    for city in cities:
        d = dist(current, city) + tsp(city, cities.difference([city]))
        L.append(d)
    return min(L)

depot = 0
cities = set([1, 2, 3, 4])
print(tsp(0, cities))
8

Why is the minimal lenght equal to \(8\)?

Solution

The distance from city \(0\) to \(1\) is \(1\), from city \(1\) to \(2\) also \(1\), and so on, until we arrive at city \(4\). Since returning to \(0\) has a cost of \(4\), the total cost is \(1 + 1 + 1 + 1 + 4\).

As an aside, the above code can be written as one list comprehension, like so:

def tsp(s: city_id, C: set[city_id]) -> length:
    if not C:
        return dist(s, 0)
    return min(dist(s, c) + tsp(c, C.difference([c])) for c in C)

Explain that the number of possible routes scales as \(O(n!)\). As a consequence, solving the TSP for any reasonable number of cities is totally infeasible.

Solution

To start the route we have \(n\) choices, for the second city we have \(n-1\) choices, etc.

The line if not cities: requires attention: it is the condition to tell the recursion to stop. More generally, when applying recursion, it is essential to ensure that eventually some stopping criterion is satisfied, for otherwise the algorithm might never stop running. Hence, when desiging recursive algorithms, always check that the algorithm contains a stopping condition.

Identify the stopping criterion for the game in which we used one die to choose uniformly a winner among 19 children.

This closes our discussion of complexity theory. Much more can be said about it, but for our purposes this suffices. We now have the means to compare the efficiency of algorithms.

2.1. Sorting

As said, we can identify the second highest bid for the Vickrey auction by sorting the bids per simulated auction. Thus, let us consider the complexity of sorting algorithms. The first, called bubblesort, is a simple, but bad algorithm; the second, called quicksort, is based on recursion and a very good algorithm.

A simple silly, idea to sort a list of \(n\) elements is like this. Compare the first element to the second element, and swap if the first is larger than the second. Then compare the second to the third and swap if necessary, and so on until the last element. Then we are sure that the largest element is moved to the last position of the list. Now apply the same strategy to the list but just up to the second to last (because the last is already in the right place), and so on. Here is the code.

def bubble_sort(A):
    for i in range(len(A)):
        for j in range(len(A) - i - 1):
            if A[j] > A[j + 1]:
                A[j], A[j + 1] = A[j + 1], A[j]  # swap
    return A

print(bubble_sort([5, 8, 2]))
[2, 5, 8]

Explain that bubblesort requires \(O(n^2)\) comparisons to sort a list of \(n\) elements.

Solution

There is a for loop in a for loop. When this is the case, the complexity is often \(O(n^2)\). Here, in particular, the first pass compares \(n-1\) elements, the second \(n-2\), and so on. The total number of comparisons is \(n-1 + n-2 + n-2 + \cdots 1 = n(n-1)/2 = O(n^{2})\).

A much faster way to sort is to use recursion. The idea is simple, but hard to invent. Take a random element of the list and call this the pivot. Then compare all elements of the list to the pivot. If an element is less than the pivot, put it in a left array, if larger into a right array, and otherwise in a middle array. Clealry, middle contains all elements equal to the pivot, so this array needs no further attention. Then apply recursively the same sorting idea to left and right.

For the code below, assume that the list has no ordering. Thus, any element will do, so we pick pivot = A[0]. 2

def quicksort(A):
    if len(A) <= 1:
        return A
    pivot, left, middle, right = A[0], [], [], []
    for x in A:
        if x < pivot:
            left.append(x)
        elif x > pivot:
            right.append(x)
        else:
            middle.append(pivot) # why not x?
    return quicksort(left) + middle + quicksort(right)

This is a recursive algorithm so we need a stopping condition. One: Which line implements this condition? Two: Why do we use arrays and not sets?

Solution

Line \(2\) is the stopping criterion. Next, sets are not necessarily ordered.

How can we estimate the complexity (i.e., the number of comparisons) of this algorithm? The for loop requires \(n\) comparisons. Then, in expectation, left and right have about \(n/2\) elements. Each requires \(n/2\) comparisons, so the second round also needs \(n\) comparisons. The third round also needs \(n\) comparisons, but over arrays with \(n/4\) elements. So, the arrays will be depleted of elements when the number of rounds \(m\) is such that \(2^{m} \leq n < 2^{m+1}\). Clearly, \(m\approx \log_2 n\), and the overall complexity must be \(O(n\log_{2}n)\). Therefore, when \(n \geq 10^{3}\), quicksort is much, much faster than bubblesort.

Let us relate this to our primary problem: finding the second largest bid in a list of \(n=10\) bids. If we use sorting for our example auction with \(n=10\) bidders, we run into a complexity of \(n \log_{2} n = \cdot 10 \log_{2} 10 \approx 40\). To find the maximum takes \(10\) comparisons in total. Now realize that we are not interested in sorting each and very row, we just want to know the second highest bid. Thus, the work we need to per row must lie somewhere between 10 and 40 comparisons.

And, indeed, we can do better when we realize that we are actually looking for the \(k\)-median, which is defined as the element in a set that has \(k\) smaller or equal elements. In particular, for the Vickrey auction we need \(k=n-1\).

What type of \(k\)-median is the normal median of a set of \(n\) elements?

Solution

For the normal median, we want to know the element such half of the set is smaller and the other half is larger. Thus, we should take \(k=n/2\) if \(n\) is even, and otherwise the average of the \(k\)th and \(k+1)\th element.

The next, pretty, algorithm to find the \(k\)th mediam borrows directly from Quicksort. Select a pivot, throw all elements of the set \(A\) that are smaller or equal to the pivot in left, and throw the rest in right. If left contains exactly \(k\) then we know that the pivot is the \(k\)-median, and we are done. Else, if left contains more than \(k\) elements, the \(k\)-median must lie in left. Otherwise, we can focus on right, but look for the \(k-l\)-median instead, where \(l\) is the number of elements of left, as all elements in left are smaller than the pivot.

def k_median(A, k):
    pivot, left, right = A[0], [], []
    for x in A:
        if x <= pivot:
            left.append(x)
        else:
            right.append(x)
    if len(left) == k:
        return pivot
    if len(left) > k:
        return k_median(left, k)
    return k_median(right, k - len(left))


x = [4, 5, 2, 8]
print(k_median(x, len(x) - 1))

What is the complexity of this \(k\)-median algorithm?

3. The Vickrey auction

After the above excursions to identify an efficient algorithm that finds the second highest number \(Y_{2}\), say in a list, we return to our initial questions about selling our car by means of a Vickrey-auction. The aim is to obtain a histogram of the density of \(Y_{2}\)}. Before actually doing the simulation, let us consider the problem from a somewhat higher perspective. Actually, we are interested in the density \(f_{2}\) of the second highest element of the order statistic of the \(\{X_{i}\}\).

Let the bids \(\{X_i\}\) have cdf \(F\), survivor function \(G\) and density \(f\). Then explain that the probability that \(Y_{2}\in [x, x+\d x]\) is given by

\begin{equation*} f_{2}(x) \d x = \P{Y_{2}\in [x, x+\d x]} = \frac{10!}{8!} (F(x))^{8} G(x) f(x) \d x. \end{equation*}
Solution

To have \(Y_{2}\in[x, x+\d x]\), one bid must lie in \([x, x+\d x]\), eight in \((-\infty, x)\) and one in \([x+\d x, \infty)\). We can order these 10 bids in \(10!\) ways, and the eight bids at the left of \(x\) in \(8!\) ways. Since all bids are independent, we can take the product of these probabilities to get the final probability.

In the next code we compute \(f_2\), carry out a simulation and plot the results. We'll use the normal and uniform distributions of scipy.

import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import norm, uniform

from latex_figures import fig_in_latex_format

fig, (ax1, ax2) = plt.subplots(1, 2, sharey="all", figsize=(6, 3))

The parameters are like this. The grid is used for plotting.

bidders = 10
k = bidders - 2
num_sim = 50
gen = np.random.default_rng(3)
grid = np.linspace(0, 1.5, num=50)

Explain that to get the second highest bid, we have to set k = bidders -2 rather than k = bidders-1.

Solution

Arrays in python start at \(0\). Hence, the last element of an \(n\) array has index \(n-1\). We are look for the second highest, and this has index \(n-2\).

We start with assuming that \(X_i\sim\Unif{[0,1]}\). We need the cdf, sf and pdf for the computation of \(f_2\) by means of the formula of the previous exercise.

X = uniform(0, 1)
f, F, G = X.pdf(grid), X.cdf(grid), X.sf(grid)
f2 = 10 * 9 * f * F**8 * G

To simulate, we generate a matrix with bids such that each of its \(N\) rows corresponds to an auction of \(n=10\) bidders, and each element is a bid uniform on \([0,1]\). To get the (simulated) value for \(Y_{2}\), we use the numpy function partition, which uses a fast algorithm to compute the \(k\)th median. The \(k\)-median is at the \(k\)th column of each row, hence the keyword axis=1.

bids = X.rvs((num_sim, bidders), random_state=gen)
sim2 = np.partition(bids, k, axis=1)[:, k]

It remains to make plots.

ax1.set_title("Uniform bids")
ax1.plot(grid, f2, color="black", label="$f_2$")
ax1.hist(sim2, bins=grid, density=True, color="gray", label="sim")
ax1.legend(loc="upper left")

We run virtually the same code for \(X_i\sim \Norm{1/2, 1/\sqrt{12}}\), and plot in Fig. 1 the density \(f_2\) and the histogram obtained by simulation. The right panel of the figure shows the histogram for a simulation with \(X_i\sim\Norm{\mu=0.5, \sigma=1/\sqrt{12}}\). Clearly, the model of the bid sizes has a major effect on the price we can get for our expensive car.

nil

Figure 1: The density \(f_2\) of the second highest bid \(Y_{2}\). In the left panel \(X_i\sim\Unif{[0,1]}\), in the right \(X_i\sim\Norm{\mu=0.5, \sigma=1/\sqrt{12}}\).

The estimated mean and std of \(Y\) for the left hand panel is \(0.81\). Can you come up with a simple explanation for the mean?

Solution

When we have 10 bidders, we have to place 10 uniform rvs on the interval \([0,1]\), hence, we divide this interval in 11 pieces, each of expected size \(1/11\). Hence, the second largest must lie at \(9/11 = 0.82\).

4. Summary

In this section we studied a bit of complexity theory. This allowed us to understand why the quicksort algorithm, based on recursion, is a much better algorithm than bubblesort. Recursion helped us also to develop a nice and efficient method to compute the $k$-median, and to write the TSP in just a few lines. We applied all this to estimating the density of the price of the second bid in an Vickrey auction, because this is the value we obtain for good sold under this auction mechanism. Finally, we realized that the second bid is actually the second value of the order statistic of a number of iid bids.

Actually, I did not expect that the density would have such a big tail to the left, as is shown in Fig. 1. Perhaps this explains why, instead of Vickrey auctions, sellers might prefer Dutch auctions in which the selling price starts very high and decreases until the first person accepts the price. However, let's not draw simplistic conclusions; we should turn to a serious book on auction theory instead. In our work we are occupied with showing the many uses of recursion, some maths, and coding.

Footnotes:

1

Read Wikipedia on the advantages and disadvantages of Vickrey auctions.

2

Choosing a robust pivot requires some thought. We'll address this in a later chapter. The speed of quicksort also depends on the order of the input. For further background, check Wikipedia.

Solving a number game with a MIP solver

1. The problem

Consider the following puzzle: Chose numbers 1 to 5 such that in the diagram below each number is contained exactly once in each row and each column. The numbers in the fields should respect any inequality between two fields. We number the fields from the left top, e.g., field \((1,5)\) contains the number 4.

nil

2. Finding a Solution

We will use pulp to solve this puzzle. The sudoku problem discussed in the pulp documentation provides some inspiration on how to tackle our problem.

from pulp import lpSum, LpVariable, LpMinimize
from pulp import LpProblem, LpStatus, value, LpInteger

prob = LpProblem("Number-Puzzle-Problem", LpMinimize)

3. The decision variables

As decision variables we use \(x_{kij}\) such that \(x_{kij} = 1\) if value \(k\) appears in the field with row \(i\) and column \(j\). The values, rows and columns are all elements of the set \(\{1, 2, 3, 4, 5\}\).

Vals = Rows = Cols =  range(1, 6)
x = LpVariable.dicts("x", (Vals, Rows, Cols), 0, 1, LpInteger)

4. Constraints with starting values

The simplest constraints deal with the numbers that are given in the diagram.

prob += x[4][1][5] == 1
prob += x[4][4][1] == 1
prob += x[2][4][5] == 1

5. Equality Constraints

There should be precisely one value in each field.

for r in Rows:
    for c in Cols:
        prob += lpSum([x[k][r][c] for k in Vals]) == 1

There can be only one value per row.

for k in Vals:
    for r in Rows:
        prob += lpSum([x[k][r][c] for c in Cols]) == 1

And also only one value per column.

for k in Vals:
    for c in Cols:
        prob += lpSum([x[k][r][c] for r in Rows]) == 1

6. Inequality Constraints

The inequality constraints of the board require more work.

Let's consider an example. In the problem diagram above we see that the value of field \((2,1)\) must be larger than the value in field \((1,1)\). Thus, if field \((2,1)\) has value 3, then field \((1,1)\) is not allowed to have a value of 3, 4, or 5. This means that, in terms of the decision variables, if \(x_{3,21}=1\) then \(\sum_{w=3}^5 x_{w,11} = 0\). More generally, we want that

\begin{equation} x_{k, 21} = 1 \Rightarrow \sum_{w=k}^5 x_{w, 11} = 0. \end{equation}

We can implement this implication with the big \(M\) trick. Choose some big \(M\), then set

\begin{equation} \sum_{w=k}^5 x_{w, 11} \leq M(1-x_{k, 21}), \end{equation}

so that if \(x_{k, 21} = 1\) then \(\sum_{w=k}^5 x_{w, 11}\) must be zero, while if \(x_{k, 21} = 0\), then \(\sum_{w=k}^5 x_{w, 11}\) is unconstrained. In code this becomes:

M = 100  # This is large enough

for k in Vals:
    prob += lpSum(x[w][1][1] for w in R[k:]) <= M * (1 - x[k][2][1])

In a similar way we implement the other inequalities.

for k in Vals:
    prob += lpSum(x[w][1][5] for w in R[k:]) <= M * (1 - x[k][1][4])
    prob += lpSum(x[w][2][5] for w in R[k:]) <= M * (1 - x[k][1][5])
    prob += lpSum(x[w][4][4] for w in R[k:]) <= M * (1 - x[k][3][4])
    prob += lpSum(x[w][4][1] for w in R[k:]) <= M * (1 - x[k][5][1])
    prob += lpSum(x[w][4][3] for w in R[k:]) <= M * (1 - x[k][4][4])
    prob += lpSum(x[w][4][5] for w in R[k:]) <= M * (1 - x[k][4][4])
    prob += lpSum(x[w][4][1] for w in R[k:]) <= M * (1 - x[k][5][1])
    prob += lpSum(x[w][5][5] for w in R[k:]) <= M * (1 - x[k][4][5])

A much simpler way (which occurred to me quite a bit later) is to define supplementary variables \(v\) that correspond to the value of the cell, i.e.,

\begin{equation} v_{ij} = \sum_{k=1}^5 k x_{kij}. \end{equation}

Then we must have, for instance, for cells \((1,4)\) and \((1,5)\) that \(v_{14}>v_{15}\). In code this becomes

v = {}
for r in Rows:
    for c in Cols:
        v[(r, c)] = lpSum([k * x[k][r][c] for k in Vals])

prob += v[(1, 5)] <= v[(1, 4)]
prob += v[(1, 1)] <= v[(2, 1)]
prob += v[(2, 5)] <= v[(1, 5)]
prob += v[(4, 4)] <= v[(3, 4)]
prob += v[(4, 1)] <= v[(5, 1)]
prob += v[(4, 3)] <= v[(4, 4)]
prob += v[(4, 5)] <= v[(4, 4)]
prob += v[(4, 1)] <= v[(5, 1)]
prob += v[(5, 5)] <= v[(4, 5)]

Note that since all cells must have different values, all strict inequalities in the constraints can be implemented as a \(\leq\) constraints.

7. Objective

The objective is trivial, because there is nothing to optimize. We only use pulp to find a feasible solution.

prob += 0, "Arbitrary Objective Function"

8. Solving the problem

Solving the problem comes down to calling the solve() method of pulp.

prob.solve()

9. The solution

Have we found a solution?

print("Status: ", LpStatus[prob.status])
Status:  Optimal

Yes! Let's print it.

for r in Rows:
    res = ""
    for c in Cols:
        for k in Vals:
            if value(x[k][r][c]) == 1:
                res += str(k) + " "
    print(res)
1 2 3 5 4
2 4 5 1 3
3 1 2 4 5
4 5 1 3 2
5 3 4 2 1

Weighted random shuffling

1. Weighted random shuffling

For some application I needed a random shuffle \(r\) of a list of numbers \(a\). The shuffle, however, should be weighted, such that, given a list of weights \(w\), the element \(a_i\) with weight \(w_i\) should be the first element of the random shuffle \(r\) with probability \(w_i/\sum_k w_k\). I searched for a python implementation of such an algorithm, but I couldn't find one. Also, numpy doesnt' provide it, and numpy.random.shuffle only offers shuffling with uniform weights. Thus, this code.

2. Weighted shuffling in pure python

Weighted random shuffling is the same as weighted random sampling from a list \(a\) without replacement. In steps:

  1. Choose with probability \(w_{i}/\sum_{i} w_{i}\) element \(a_{i}\) from \(a\).
  2. Add this element to a list \(r\).
  3. Remove element \(a_{i}\) from \(a\) and \(w_{i}\) from \(w\).
  4. Stop if \(a\) is empty, else,
  5. Return to step 1 with the updated lists \(a\) \(w\).

However, I found the code below for weighted random sampling on this page.

from random import random

def weighted_choice(weights):
    rnd = random() * sum(weights)
    for i, w in enumerate(weights):
        rnd -= w
        if rnd < 0:
            return i

Thus, weighted_choice() gives an index to the chosen element of an array. With this index I can fill a random shuffle \(r\) with elements of \(a\).

As removing elements from lists is a bit expensive, I chose to set the weight \(w_{i}\) of the chosen element to zero. This ensures that the element \(i\) cannot be selected the next time.

def weighted_shuffle(a,w):
    w = list(w) # make a copy of w
    if len(a) != len(w):
        print("weighted_shuffle: Lenghts of lists don't match.")
        return

    r = [0]*len(a) # contains the random shuffle
    for i in range(len(a)):
        j = weighted_choice(w)
        r[i]=a[j]
        w[j] = 0
    return r

3. Fast shuffling

Once I had the above code, I went to Gerlach van der Heide. He managed to speed it up by several orders of magnitude.

from random import random
from bisect import bisect_right
import numpy as np

def weighted_shuffle(a,w):
    r = np.empty_like(a)
    cumWeights = np.cumsum(w)
    for i in range(len(a)):
         rnd = random() * cumWeights[-1]
         j = bisect_right(cumWeights,rnd)
         #j = np.searchsorted(cumWeights, rnd, side='right')
         r[i]=a[j]
         cumWeights[j:] -= w[j]
    return r

a = np.arange(1,1000)
w = np.arange(1,1000)
r = weighted_shuffle(a,w)

Here is the result

print(r[:2])

Some testing shows, for a reason unknown to me, that np.searchsorted is slower than bisect_right.

4. A wrong idea

While searching the web I came across the one-liner below. It is elegant, but wrong. The weights according to which the elements of \(a\) are chosen are not in proportion to \(w\). Some basic probability theory shows why this is the case. It comes down to proving that \[P\{w_1 U_1 \leq w_2 U_2\} \neq \frac{w_1}{w_1+w_1},\] where \(w_i\) are the weights and \(U_i\) uniform random deviates on \([0,1]\).

def wrong(a,w):
    aa = range(len(a))
    aa.sort(key=lambda i: w[i]*random()*beta[i])
    return [a[i] for i in aa]

Memoization with Python

1. Content of a Discrete Hyper Pyramid

We like to compute the number of possibilities \(\P{n, N}\) for \(x = (x_1, \ldots x_n)\) such that \(x_i \in \{0,1,\ldots, N\}\) and \(\sum_i x_i \leq N\). It is easy to see that \(\P{n, N}\) satisfies the recursion: \[\P{n, N} = \sum_{i=0}^N \P{n-1, N-i},\] with boundary conditions \(\P{1, N} = N+1\) for all \(N\). Note that by using the summation above, this condition can be replaced by \(\P{0, N} = 1\) for all \(N\).

Computing \(\P{n,N}\) is easy when we use memoization. In fact, we can code the compuation in nearly one line!

from functools import lru_cache

@lru_cache(maxsize=128)
def P(n, N):
   return (n==0) or sum( P(n-1, N-i) for i in range(N+1) )

n=5
N=80
print(P(n = 5, N = 80))
32801517

2. A probability problem

We throw multiple times with a coin that lands heads with probability \(p\). What is the probability \(\P{n,k}\) to see at least \(k\) heads in row when you throw a coin \(n\) times?

A bit of thinking shows that \(\P{n,k}\) must satisfy the recursion \[\P{n,k} = p^k + \sum_{i=1}^k p^{i-1} q\, \P{n-i,k},\] because it is possible to throw \(k\) times heads from the first throw, but otherwise you throw \(i\), \(i < k\), times a heads, then a tails, after which you have to start all over again.

Reasoning similarly, the expected number times \(\E{n,k}\) to see at least \(k\) heads in row when you throw a coin \(n\) times must satisfy the recursion \[\E{n,k} = p^k(1+\E{n-k,k}) + \sum_{i=1}^k p^{i-1} q\, \E{n-i,k}.\]

p = 0.5
q = 1. - p

@lru_cache(maxsize=128)
def P(n, k):
    """
    probability to see at least k heads in row when a coin is thrown n times
    """
    if n < k:
        return 0
    else:
        return sum(P(n-i,k) * p**(i-1) * q for i in range(1,k+1)) + p**k

@lru_cache(maxsize=128)
def E(n, k):
    """
    expected number of times to see at least k heads in row when a coin is thrown n times
    """
    if n < k:
        return 0
    else:
        tot = sum(E(n-i,k) * p**(i-1) *q for i in range(1,k+1))
        tot += p**k * (1 + E(n-k,k))
        return tot



k = 2

for n in range(k,10):
    print(n, P(n,k), E(n,k))

links in nikola and orgmode

I just started using nikola. Of course I want to refer from one page (or post) to another, but I could not find out how. After sending an issue, I got great and fast help. And now I know how to do it.

If things are not yet solved in the nikola plugin for orgmode, put this as the end of the init.el of the plugin:

(org-link-set-parameters
 "link"
 :export (lambda (path desc backend)
           (cond
            ((eq 'html backend)
             (format "<a href=\"link:%s\">%s</a>"
                     path (or desc path)))))
)

And now you can refer to any page like so

link:/bio

or

[[link:/bio][My bio page]]

latex-pythontex-matplotlib-tikzplotlib

1. The problem

Sometimes I want to use a python script to produce a plot and include this plot in \(\LaTeX\). My usual approach was like this. First I wrote a python script that, after some computations, exported the plot to a pdf file (or some other format). Then I imported the pdf file within a figure environment in a \(\LaTeX\) file. Thus, I had three files, of which two were kind of superfluous, i.e., the python script and the pdf file with the figure. And, after some time, I typically forgot which python script I used to make which pdf file, so I had to include comments in my \(\LaTeX\) file to explain where to find what, and what did what.

And then there came a day that I did not like this anymore.

2. My solution

I want one file .tex file that contains all: obviously the story itself, but also the python and the figure, provided of course it takes not a lot of time to make the data for the figure. After some searching and testing, here is an MWE of how I am doing things now with pythontex, matplotlib, and tikzplotlib.

I use arara to compile the LaTeX file, but this just handy, not necessary for the rest to work.

2.1. One option.

If you want to run the code, but don't want to show it, use the pycode environment, as I do here.

% arara: pdflatex: { shell: yes }
% arara: pythontex: {verbose: yes, rerun: modified }
% arara: pdflatex: { shell: yes }

\documentclass{article}
\usepackage{pythontex}
\usepackage{pgfplots}

\begin{document}
\maketitle

Here is text.

\begin{figure}[h]
\centering
\begin{pycode}

from matplotlib.pylab import plt
import tikzplotlib

plt.plot([1, 2, 3], [1, 5, 2], label="x")

plt.legend()
print(tikzplotlib.get_tikz_code(axis_height="5cm", axis_width="6cm"))
\end{pycode}
\caption{It works.}
\end{figure}
\end{document}

The trick is to not write the output of tikzplotlib to a file, but have it printed as output of the pycode environment. Note that this is an MWE; for a good figure you'll need to tune it to your needs.

2.2. Another option

If you like to include in the main text the code to show how you made the graph, use the pyblock environment, and then \printpythontex within a figure environment.

% arara: pdflatex: { shell: yes }
% arara: pythontex: {verbose: yes, rerun: modified }
% arara: pdflatex: { shell: yes }

\begin{document}

Here is text.

\begin{pyblock}
from matplotlib.pylab import plt
import tikzplotlib

plt.plot([1, 2, 3], [9, -1, 8], label="x")

plt.legend()
print(tikzplotlib.get_tikz_code(axis_height="5cm", axis_width="6cm"))
\end{pyblock}

\begin{figure}[h]
  \centering
  \printpythontex
\end{figure}

\end{document}

My first post

Table of Contents

Here is my first post

I want to write posts and a homepage in org mode. (I dislike markdown, and I detest typing math in rst.) After watching one of the blogs on Cest la Z I considered nikola as a good option to set things up. It turned out to be really easy; I just followed the steps as described here. I had to install some python packages, but with pip this was a no brainer.

There was one caveat with including source code blocks in org files. After reading the error messages, I noticed that I had to install the emacs htlmize package. The init.el and myinit.org files on my emacs repo show how to set this up.

And now all works!