This whole tutorial starts with a simple example problem

Suppose we have 99 empty papers and we wrote numbers from 1 to 99 (using each number) on one side of the papers randomly. Then we mixed all the papers randomly and started to write numbers from 1 to 99 on the other sides (empty sides) of each paper randomly. What is the probability of having the same number on both sides of at least one paper?

https://math.stackexchange.com/questions/394007/a-tricky-probability-question

Give it a go and quickly you will see how tricky it is. The answer requires an understanding of derangements and we will go through this idea in detail in this post.

What is a derangement?

So what is a derangement? Consider a set of integers

1 2 3 4 5

A permutation is any ordering of these elements. For a list of $n$ integers; there are $n!$ permutations. A permutation may also be called an arrangement. A derangement is a permutation/arrangement in which none of the integers appear in their correct position.

2 3 5 1 4  # Example derangement

From this description you may be able to see that the problem posed at the start is closely related to the number of derangements of the sequence of all integers up to 100.

We will take a concrete example that you can reference throughout (see the appendix for the code used to produce this) involving a set with 5 elements. Each column refers to the number of elements in their correct position.

   0      1      2      3      4      5      
===========================================
|BADEC |ACBED |ABDEC |ABCED |      |ABCDE |
|BAECD |ACDEB |ABECD |ABDCE |      |      |
|BCAED |ACEBD |ACDBE |ABEDC |      |      |
|BCDEA |ADBEC |ACEDB |ACBDE |      |      |
|BCEAD |ADEBC |ADBCE |ADCBE |      |      |
|BDAEC |ADECB |ADCEB |AECDB |      |      |
|BDEAC |AEBCD |AEBDC |BACDE |      |      |
|BDECA |AEDBC |AECBD |CBADE |      |      |
|BEACD |AEDCB |BCADE |DBCAE |      |      |
|BEDAC |BACED |BDCAE |EBCDA |      |      |
|BEDCA |BADCE |BECDA |      |      |      |
|CABED |BAEDC |CABDE |      |      |      |
|CADEB |BCDAE |CBDAE |      |      |      |
|CAEBD |BCEDA |CBEDA |      |      |      |
|CDAEB |BDACE |DACBE |      |      |      |
|CDBEA |BDCEA |DBACE |      |      |      |
|CDEAB |BEADC |DBCEA |      |      |      |
|CDEBA |BECAD |EACDB |      |      |      |
|CEABD |CADBE |EBADC |      |      |      |
|CEBAD |CAEDB |EBCAD |      |      |      |
|CEDAB |CBAED |      |      |      |      |
|CEDBA |CBDEA |      |      |      |      |
|DABEC |CBEAD |      |      |      |      |
|DAEBC |CDABE |      |      |      |      |
|DAECB |CDBAE |      |      |      |      |
|DCAEB |CEADB |      |      |      |      |
|DCBEA |CEBDA |      |      |      |      |
|DCEAB |DABCE |      |      |      |      |
|DCEBA |DACEB |      |      |      |      |
|DEABC |DBAEC |      |      |      |      |
|DEACB |DBEAC |      |      |      |      |
|DEBAC |DBECA |      |      |      |      |
|DEBCA |DCABE |      |      |      |      |
|EABCD |DCBAE |      |      |      |      |
|EADBC |DECAB |      |      |      |      |
|EADCB |DECBA |      |      |      |      |
|ECABD |EABDC |      |      |      |      |
|ECBAD |EACBD |      |      |      |      |
|ECDAB |EBACD |      |      |      |      |
|ECDBA |EBDAC |      |      |      |      |
|EDABC |EBDCA |      |      |      |      |
|EDACB |ECADB |      |      |      |      |
|EDBAC |ECBDA |      |      |      |      |
|EDBCA |EDCAB |      |      |      |      |
|      |EDCBA |      |      |      |      |
===========================================
   44     45     20     10     0     1     
$$$$ We will start with the equation for the number of derangements and then go about deriving and exploring it in more detail. The number of derangements for a set with $n$ elements is; $$$$ \begin{equation} D(n) = n! \sum_{i=0}^n \frac{(-1)^i}{i!} \tag{1} \end{equation} $$$$ The notation $!n$ is often used instead of $D(n)$. In the limit of large $n$ this becomes $$$$ \begin{align} !n &= n! e^{-1} + \mathcal{O}\left(\frac{1}{(n+1)!} \right) \\ & \approx n! e^{-1} \end{align} $$$$ Which is a surprising result; it says that if we randomly choose the arrangement of elements there is a 36% chance they will all be wrong, independently from n. That seems counterintuitive (to me anyway). Take $n=10$. The probability that exactly one or none are correct is $73\%$; hence the probability of at least 2 being correct is $27\%$. So if I picked ten cards, recorded their order, then shuffled them. By far the most likely outcome is that one or fewer cards are correct; so the odds are in my favour to bet you won’t get two cards right. The odds don’t change if I increase the game to 15 cards. $$$$ This result also has implications for generating derangements in code; just generate random arrangements and check the result. Sooner or later it will be a derangement.

Getting it Wrong (1)

It is instructive to actually get the answer wrong by using a common mistake which is nevertheless pretty accurate and simple. $$$$ Imagine picking one number at a time for each of the number “slots” as outlined in the original problem. The probability of getting a wrong number on the first slot is $(n-1)/n$, therefore the probability of $n$ of these incorrect choices in succession is \begin{equation} \left( \frac{n-1}{n} \right)^n \tag{2} \end{equation} This is wrong because it doesn’t take account of the conditional probabilities. $$$$ It only works if the bag of elements you are selecting from is constantly replenished; but in our problem there is an “arrow of time” in that subsequent elements being picked are affected by what was picked previously. However; consider the following limit $$\lim_{\epsilon \rightarrow 0 } \ (1-\epsilon)^{1/\epsilon} \approx 1/e \ ,$$ where $\epsilon = 1/n$, it is identical to the limit of the correct answer Eq.(1). This is interesting given that the logic of this answer is incorrect. $$$$ The reason is that the approximation of the probability of the incorrect choice being $(n-1)/n$ is accurate for large $n$. The lack of a replacement into the pool of numbers is not noticeable. $$$$ Take for example $n=4$, we slightly underestimate the probability of picking a derangement by using Eq.(2), \begin{align} D(4) & \approx 0.3333 \ , \\ \left( \frac{3}{4} \right)^4 & \approx 0.3164 \ , \end{align} that’s because once we’ve picked an incorrect element; the chances of an incorrect element for subsequent picks goes ever-so-slightly up because the first element has guaranteed that another element must be incorrect. Eq.(2) does not include this effect. $$$$ To be even more explicit, let’s say the first element chosen is a $2$. The next element (the slot for $2$) is automatically incorrect with 100% probability. Of course the first element could also have been a $3$ or $4$, then the next elements chances are lower than $3/4$ for an incorrect selection. But overall, the two effects pretty much balance out to make $\frac{n-1}{n}$ a good approximation for the probability of any choice being incorrect.

Getting it Right (1)

The last section has shown that it is crucial to account for the conditional probability in order to get the right answer. $$$$ To factor in conditional probability, lets go through the selection process of a single permutation one slot at a time and build the total number of derangements up; $$$$ (1) There are $n-1$ choices for the first element which are incorrect. $$$$ (2) For the second element there are two scenarios; $$$$ (2a) the first choice was actually the second element and therefore no matter what choice is made for the second slot it must be incorrect ($n-1$ options) or $$$$ (2b) the bag of elements still contains the second, leaving $n-2$ incorrect choices. $$$$ So every choice leads to a branching into the next choice. If we drew this as a “decision tree’ there would be $2^n$ end leaves (let’s not do this!) $$$$ Instead, because each choice relies on its previous one we can create a recurrence relation by combining (2a) and (2b). We will use notation $D(n)$ for a derangement of size $n$; $$$$ \begin{align} D(n) = (n-1) D(n-1) + (n-1) D(n-2) \tag{3} \end{align} $$$$ This leads to (applying the formula to itself recursively until reaching the end of the sequence where $D(2)=1$ and $D(1)=0$) $$$$ \begin{align} D(n) – n D(n-1) &= – (D(n-1) – (n-1) D(n-2)) \\ &= (-1)^2 (D(n-2) – (n-2) D(n-3)) \\ &= \cdots \\ &= (-1)^n-2 (D(2) – 2 D(1)) \\ &= (-1)^{n-2} \\ D(n) &= n D(n-1) + (-1)^n \tag{4} \end{align} $$$$ which is a second useful recurrence relation then defining the probability of derangement as; $$ p = \frac{D(n)}{n!} $$ which is the chance of randomly selecting a permutation that is a derangement. Then we have $$ p_i – p_{i-1} = \frac{(-1)^i}{ i!} \ .$$ Now we form the sum over all $i$; $$ \sum_{i=1}^{n} p_i – p_{i-1} = -p_0 + p_{n} = \sum_{i=1}^{n} \frac{(-1)^i}{ i!} \ ,$$ and since $p_0 = 1$ then we have that $$ D(n) = n! + n! \sum_{i=1}^{n} \frac{(-1)^i}{i!} \ . $$ Explicitly including the $n!$ as the zeroth element of the sum we get the derangement equation Eq.(1) $$ D(n) = \sum_{i=0}^{n} \frac{(-1)^i}{i!} $$

Getting it Right (2)

The previous derivation required us to think about how to form a derangement recursively. We can actually derive it in a different way by focussing on the combinatorics. $$$$ The total number of ways to get everything wrong is the total number of permutations ($n!$) minus the number where at least one is correct; We will use $P$ to denote permutations (not to be confused with probability) and $c$ the number of correct assignments in the permutation \begin{align} P(c=0) &= n! – P(c \geq 1) \\ & = n! – P(c=1) – P(c=2) – \cdots – P(c=n) \end{align} We can now try to count each of these permutations. When $c=n$ all elements are correct and this happens only one way; $$ P(n) = 1 \ . $$ What if all but one of the assignments are correct? This is impossible; since the only way to make a label incorrect is to swap two labels; making at least two incorrect; $$ P(n-1) = 0 \ . $$ For two assignments to be incorrect, we would just swap two correct elements from the fully-correct permutation $$ P(n-2) = \frac{n (n-1)}{ 2!} \ . $$ To pick three incorrect we would swap two correct assignments and then swap one of the now incorrect entries with a correct one. We also have to take account of the derangements of the incorrect subset, $$ P(n-3) = \frac{n(n-1)(n-2)}{3!} D(3) \ . $$ Looking at this sequence you can derive a general rule starting from $c=n-2$, since $D(2)=1$ \begin{align} P(n-2) &= \frac{n (n-1)}{ 2!} D(2) \\ P(n-3) &= \frac{n(n-1)(n-2)}{3!} D(3) \\ \vdots \\ P(n-m) &= \frac{n!}{m! (n-m)!} = {n \choose m} D(m) \end{align} the factor at the front of these expressions is the binomial coefficient! $$$$ So we now have a general formula for all the terms in the original sum, so let’s return to that; \begin{align} D(n) &= n! – \sum_{k = 1}^{n-1} {n \choose k} D(k) \\ n! &= D(n) + \sum_{k = 1}^{n-1} {n \choose k} D(k) \\ n! &= \sum_{k = 1}^{n} {n \choose k} D(k) \\ \end{align} this feels close… But you must now take a bit of a leap and discover the trick, binomial inversion. Binomial inversion is a relationship between a sum like the one above that involves a binomial coefficient. The rule is as follows; \begin{align} b_n &= \sum_{i=0}^n {n \choose i} a_i \\ a_n &= \sum_{i=0}^n(-1)^{n-i} {n \choose i} b_i \end{align} All we need to do it apply this and follow the algebra and the main derangement Eq.(1) will appear; \begin{align} D(n) &= \sum_{i = 0}^{n} (-1)^{n-i} {n \choose i} (i!) \\ D(n) &= \sum_{i = 0}^{n} (-1)^{n-i} \frac{n!}{ (n-i)!} \\ p(n) &= \sum_{j= n}^{0} \frac{(-1)^{j} }{j!} \\ p(n) &= \sum_{j= 0}^{n} \frac{(-1)^{j} }{j!} \\ \end{align}

Proof using PIE

I’ve left this one to the end because it is the most common proof you will come across elsewhere. It also requires the use of another “leap of faith” type principle you may not have come across before, the principle of inclusion-exclusion. $$$$ We define $S_k$ to be the set of permutations of elements where element $k$ is fixed; meaning that there are $(n-1)!$ elements in each $S_k$. $$$$ Clearly these sets are not mutually exclusive. Since sets $S_i$ and $S_j$ for $i \neq j$ have overlapping elements. The set formed by $S_i \cap S_j$ has $(n-2)!$ elements, $S_i \cap S_j \cap S_k$ for $i \neq j \neq k$ has $(n-3)!$ elements and so forth. $$$$ Now the inclusion-exclusion principle states that $$\left| S_1 \cup S_2 \cup … \cup S_n \right| = \sum_{k=1}^n(-1)^{k+1} \bigg( \sum_{1 \leq i_1 \leq \cdots \leq i_k < n} |S_{i_1} \cap ... \cap S_{i_{k}}| \bigg)$$ For each term, a set number of the elements are fixed, and the number of elements within the term in $(n-k)!$ as we have seen previously. Due to the arrangement of indices $i_1, \cdots, i_k$ there are ${n \choose k}$ ways of choosing the fixed elements so the series is written \begin{align} |S_1 \cup S_2 \cup ... \cup S_n| &= {n \choose 1} (n-1)! - {n \choose 2} (n-2)! + {n \choose 3} (n-3)! - \cdots \\ & = n! \left( \frac{1}{1!} - \frac{1}{2!} + \frac{1}{3!} - \cdots \right) \\ & = n! \sum_{i=1}^n \frac{(-1)^i}{i!} \end{align}

Example Problems using Derangements

We have now seen in detail how to answer the problem posed at the start of the post. In practice there are variants on this problem which differ by more than just semantics. $$$$ Often, you may want to find the number of permutations where “exactly $k$ out of $n$ are correct”; this would be given by $$ P(k) = {n \choose k} D(n-k) $$ (1) for example, $n=7$, what is probability that selecting a random permutation, exactly three are correct? \begin{align} N &= {7 \choose 3} * D(4) \\ &= 7*5 * D(4) = 35 * 24 * (1 + 1/2 – 1/3 + 1/4) \\ &= 35 * 24 * (24 +12 – 8 + 6)/24 = 35 * 9 = 315 \\ P &= \frac{N}{7!} \approx 6.25 \% \end{align} Questions which amount to a statement about derangements : $$$$ (2) “How many ways can we pair up a series of partners such that nobody ends up with their partner.” $$$$ This is simply the number of derangements; visualize giving each dancer and their true partner the same number. $$$$ (3) “In how many ways can the integer 1,2,3,…10 be arranged in a line so that no even integer is in its natural position?” $$$$ In this case the answer can be found by the total number of permutations, minus the number where exactly one integer is in its correct position, minus the number of permutations with exactly two, …, all the way to five. \begin{align} & = 10! – {10 \choose 1} D(9) – {10 \choose 2} D(8) – {10 \choose 3} D(7) – {10 \choose 4} D(6) – {10 \choose 5} D(5) \\ & = 10! – 10 D(9) – 45 D(8) – 120 D(7) – 210 D(6) – 252 D(5) \\ \end{align}

Derangements using Python

This was a maths-heavy post so far, which is good for Olympiad style maths problems; but there are also potentially occasions where derangements are needed in code. If nothing else; having the code allows you to check solutions to problems.

So, let’s write some code to calculate the total number of derangements.

The first option is to just “brute force” it; let’s enumerate every possible permutation and check for each one whether the permutation is a derangement or not.

Below is the code

import numpy as np
import math
from itertools import permutations

def brute_force_derangement(n):
    
    a = np.arange(1,n,1)

    deranged = 0
    for perm in permutations(a):
        if not np.any(perm == a):
            deranged += 1
            
    return deranged / math.factorial(n)

This certainly does the job and is very explicit; let’s see how long it takes for n=10 (on my machine, MacBook with a core i5 processor)

In [0]:
%timeit brute_force_derangement(10)
4.55 s ± 244 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Ok, a few seconds is reasonable for a quick “back of the envelope” code check. But this won’t cut it for a real application; and it isn’t scalable at all.

Lets dig into the scalability a bit more…

First of all, there is a factorial growth of executions the for loop. That’s going to kill the algorithm for n a little above 10. Even the algorithm in the loop itself is inefficient; it requires an element by element check of a list, which grows as O(n).

We could use our analytical solution derived in Eq.(1). In principle this is a good idea; but you will find that it requires the code to deal with some large numbers (via the factorial) which may slow things down. Let’s code it and check

def derangement_formula(n):
    
    tot= 0
    for i in range(n):
        tot += (-1)**i / math.factorial(i)
    return int(math.factorial(n)*tot)
In [0]:
%timeit derangement_formula(10)
6.4 µs ± 261 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Ok, pretty good! a million times faster. That is a victory for the maths; analytical derivation of a formula has prevented our code requiring a exhaustive brute force solution. Now let’s grab a victory for the code.

So how do we improve this? Although the recursion formula we derived earlier was of limited use in practical problems (our brains don’t really do recursion very well) aside from its use in derivation. Let’s try the first recursive formula Eq.(4)

def derangement_recursive_1(n):
    current_n   = 1
    current_d_n = 1
    while current_n < n:
        current_d_n = current_n * current_d_n + (-1)**current_n
        current_n += 1
        
    return current_n
In [0]:
%timeit derangement_recursive_1(10)
5.01 µs ± 606 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Ok this has sped things up a bit; but not substantially. What is holding us up? Well, generally the power operation has a high computational cost. Multiplication and addition are lower on computation cost.

So let's try to remove the power; in this case it's very easy since all that's happening is that each term alternates between positive and negative. This can be done with multiplication, but it requires one extra variable to be held in memory. In this case it will benefit us...

def derangement_recursive_2(n):
    current_n   = 1
    current_d_n = 1
    prev = 1
    while current_n < n:
        prev *= -1
        current_d_n = current_n * current_d_n + prev
        current_n += 1
        
    return current_n
In [0]:
%timeit derangement_recursive_2(10)
1.76 µs ± 86.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

More than twice as fast!

Can we do even better? Within each iteration of the loop there are 2 multiplies and 2 adds. We can reduce this to one multiply and three add by using the other recursion relation Eq.(3)

def derangement_recursive_3(n):
    current_n             = 1
    current_d_n           = 1
    current_d_n_minus_one = 1 
    while current_n < n:
        tmp = current_d_n
        current_d_n = (current_n-1) * (current_d_n + current_d_n_minus_one)
        current_d_n_minus_one = tmp
        current_n += 1
        
    return current_n
In [0]:
%timeit derangement_recursive_3(10)
1.71 µs ± 42.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

This doesn't really result in much gain. However we can re-organise to reduce the number of adds operations to two

def derangement_recursive_4(n):
    current_n             = 1
    current_d_n           = 1
    current_d_n_minus_one = 1 
    while current_n < n:
        tmp = current_d_n
        current_n += 1
        current_d_n = current_n * (current_d_n + current_d_n_minus_one)
        current_d_n_minus_one = tmp
        
        
    return current_n
In [0]:
%timeit derangement_recursive_4(10)
1.63 µs ± 95.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Now the gains we are seeing are very modest and we should probably stop.

To tie it all off, I have run these functions through 10 repeats of the timeit function for n between 2 and 20

Appendix : Code

Here is the code used to produce the ASCII style example of all permutations of set at the start of this post

import numpy as np

def pretty_print_permutations(a):
    n = len(a)
    
    count    = 0 
    deranged = []
    corr = []

    for d in permutations(a):
        corr     += [sum(list(d) == a)]
        deranged += ["".join(d)]

    picks = dict()
    for i in range(n+1):
        examples = []
        for index in np.argwhere(np.array(corr) == i):
            examples += [deranged[index[0]]]
        picks[i] = examples

    columns = n+1

    text = " "*(int(n/2)+1) + "".join(["{}".format(i) + " "*(n+1) for i in range(n+1)]) + "\n"
    linelen = len(text)
    text += "=" * (linelen-3) + "\n"

    max_i = max([len(x) for x in picks.values()])
    for i in range(max_i):
        line = ""
        for j in range(columns):
            try:
                line += "|" + str(picks[j][i]) + " "
            except:
                line += "|" + " "*(n+1)   
        line += "|\n"
        text += line

    text += "=" * (linelen-3) + "\n"
    text += " "*(int(n/2)+1) + "".join(["{}".format(len(i)) + " "*n for i in picks.values()]) + "\n"
    
    return text

And here is the code used to create the speed test plot;

import timeit
import numpy as np
import math
from itertools import permutations
import matplotlib.pyplot as plt

"""
A speed test of various ways to calculate the derangement probability 
"""


def brute_force_derangement(n):
    """
    Find the probability of a derangement being found
    from a random permutation of n elements by a brute
    force search
    """
    a = np.arange(1, n + 1, 1)

    deranged = 0
    for perm in permutations(a):
        if not np.any(perm == a):
            deranged += 1

    return deranged / math.factorial(n)


def derangement_formula(n):
    """
    Find the probability of a derangement being found
    from a random permutation of n elements using the
    standard analytic form
    """
    tot = 0
    for i in range(n):
        tot += (-1) ** i / math.factorial(i)
    return int(math.factorial(n) * tot)


def derangement_recursive_1(n):
    """
    Find the probability of a derangement being found
    from a random permutation of n elements using a
    recursive formulae
    """
    current_n = 1
    current_d_n = 1
    while current_n < n:
        current_d_n = current_n * current_d_n + (-1) ** current_n
        current_n += 1

    return current_n


def derangement_recursive_2(n):
    """
    Find the probability of a derangement being found
    from a random permutation of n elements using a
    recursive formulae
    """
    current_n = 1
    current_d_n = 1
    prev = 1
    while current_n < n:
        prev *= -1
        current_d_n = current_n * current_d_n + prev
        current_n += 1

    return current_n


def derangement_recursive_3(n):
    """
    Find the probability of a derangement being found
    from a random permutation of n elements using a
    recursive formulae
    """
    current_n = 1
    current_d_n = 1
    current_d_n_minus_one = 1
    while current_n < n:
        tmp = current_d_n
        current_d_n = (current_n - 1) * (current_d_n + current_d_n_minus_one)
        current_d_n_minus_one = tmp
        current_n += 1

    return current_n


def derangement_recursive_4(n):
    """
    Find the probability of a derangement being found
    from a random permutation of n elements using a
    recursive formulae
    """
    current_n = 1
    current_d_n = 1
    current_d_n_minus_one = 1
    while current_n < n:
        tmp = current_d_n
        current_n += 1
        current_d_n = current_n * (current_d_n + current_d_n_minus_one)
        current_d_n_minus_one = tmp

    return current_n


if __name__ == "__main__":

    def get_times(func, n_values, repeats=1):
        res = np.zeros(len(n_values))
        for r in range(repeats):
            res += [timeit.timeit(lambda: func(n)) for n in n_values]
        return np.vstack((n_values, res / repeats))


    n_values = np.arange(2, 21, 1)

    d1 = get_times(derangement_formula, n_values, repeats=10)
    d2 = get_times(derangement_recursive_1, n_values, repeats=10)
    d3 = get_times(derangement_recursive_2, n_values, repeats=10)
    d4 = get_times(derangement_recursive_3, n_values, repeats=10)
    d5 = get_times(derangement_recursive_4, n_values, repeats=10)

    np.savetxt("speed_test.csv", np.hstack((n_values, d1[0], d2[0], d3[0], d4[0], d5[0])), delimiter=",")

    fig, ax = plt.subplots(1, 1, figsize=(8, 8))

    ax.plot(d1[0], d1[1], label="Standard Formula", color="black")
    ax.plot(d2[0], d2[1], label="Recursive 1", color="b")
    ax.plot(d3[0], d3[1], label="Recursive 2", color="r")
    ax.plot(d4[0], d4[1], label="Recursive 3", color="g")
    ax.plot(d5[0], d5[1], label="Recursive 4", color="purple")

    ax.scatter(d1[0], d1[1], color="black")
    ax.scatter(d2[0], d2[1], color="b")
    ax.scatter(d3[0], d3[1], color="r")
    ax.scatter(d4[0], d4[1], color="g")
    ax.scatter(d5[0], d5[1], color="purple")

    ax.set_title(r"$\rm Timings \ for \ derangement \ implementations $", fontsize=18)
    ax.set_xlabel(r"$n$", fontsize=18)
    ax.set_ylabel(r"$\rm Time \ (\mu s)$", fontsize=18)

    ax.set_xlim([1, 20])
    ax.set_ylim([0, 25])

    ax.legend(frameon=False, fontsize=16)
    ax.minorticks_on()
    ax.set_xticks(n_values)

    fig.savefig("derangement_speed_test.pdf")

    plt.show()