The Centrifuge Problem
A while ago (almost 3 years ago, to be more exact) I watched a video on Numberphile where Dr Holly Krieger presents the following problem.
Given a centrifuge with
holes, is it possible to balance test tubes?
Mathematical solution
On the video Dr Krieger goes over her thinking process, and some of the intuitive (and not so intuitive) ways of constructing solutions. Particularly, a solution is valid if and only if both the number of test tubes,
The main takeaway, for me, is this problem can be thought of as finding the subset
where
For simplicity, if we take
Finding roots of unity (
Computational solution
As an exercise to refresh my Mathematica programming and graphing skills, I decided to solve this problem using Wolfram Mathematica.
The problem, from a computational point of view, can be divided into main the following problems:
- Find the set of roots,
. - Find all possible subsets
. - Check which subsets are valid solutions.
Note
I developed this code back in February-March 2019. I just never found the time to sit down and write about it.
Finding the roots
Finding the
MyFindRoots[z_, n_] := Module[{roots, r, angle},
roots = ConstantArray[0, n];
For[k = 0, k < n, k++, {
r = Abs[z],
angle = Arg[z],
r = r^(1 / n),
roots[[k + 1]] = r * Exp[I * (angle + 2 * Pi * k) / n]
}];
roots
]
As an example, we can find the 5th roots of unity shown in the previous section:
A native alternative to MyFindRoots
would be to use the Solve
function:
However, this function is slower in the range of
Needs["GeneralUtilities`"]
f1 = MyFindRoots[1, #] &;
f2 = MyFindRootsNative[1, #] &;
BenchmarkPlot[{f1, f2}, # &, Range[100], "IncludeFits" -> True, TimeConstraint -> 60]
For this reason, we would choose MyFindRoots
over the Solve
-powered MyFindRootsNative
function.
Finding the subsets
One of the awesome things about Mathematica is that operations in set theory are trivial. To get all subset
which results in the list of all possible
{{}, {1}, {I}, {-1}, {-I}, {1, I}, {1, -1}, {1, -I}, {I, -1}, {I, -I}, {-1, -I}, {1, I, -1}, {1, I, -I}, {1, -1, -I}, {I, -1, -I}, {1, I, -1, -I}}
Checking valid subsets
The problem here will be, thus, checking if the solutions are valid. To calculate the sum of all elements of each subset
This example shows that, for
Taking all this into account, a simple function to find all possible solutions can be the following:
FindCentrifugeSols[n_] := Module[{solutions, roots, subsets, subset, total, epsilon, i},
roots = UsedFindRoots[1, n];
subsets = Subsets[roots, n];
epsilon = Power[10, -15];
solutions = {};
(*Main loop*)
For[i = 1, i <= Length[subsets], i++, {
subset = subsets[[i]],
total = Total[subset],
If[Abs[N[total]] < epsilon,
solutions = Append[solutions, subset]
]
}];
(*Return*)
{roots, solutions}
]
Let's see it action (note that it returns the set of roots
To better visualise the solutions, I wrote a function1 to draw the centrifuge configurations:
An obvious thing to notice here is that for
Here's where things get a bit more complicated.
Reducing the solutions
Up to this point, we've already solved the Centrifuge Problem. However, we want to reduce the solutions to truly unique configurations of a centrifuge.
The gist of this step is that we need to rotate all solutions and compare them to each other to get rid of duplicated ones. To accomplish this, we will introduce a set of auxiliary functions:
RotateSol
.SortSetClockwise
.ClassifyCentrifugeSols
.
This function simply applies a rotation of
SortSetClockwise[set_] := Module[{angles, order},
angles = Map[Arg, set];
order = Ordering[angles, All, NumericalOrder];
angles = angles[[order]];
Map[Exp, I * Pi * Rationalize[N[angles] / Pi]]
]
This step is probably the most important one to minimise calculations. Since we'll be comparing solutions in set form as a whole, we need to make sure they follow a specific, deterministic structure.
ClassifyCentrifugeSols[n_] := Module[{MyMat, MyVect, roots, subsets, subset, epsilon, solutions, curr, next, i},
solutions = FindCentrifugeSols[n];
subsets = Sort[solutions[[2]]];
roots = solutions[[1]];
epsilon = Power[10, -15];
MyMat = {};
MyVect = {};
(*Main loop*)
For[i = 1, i <= Length[subsets], i++, {
subset = subsets[[i]],
curr = Length[subset];
(*Check length of next element*)
If[i + 1 <= Length[subsets],
next = Length[subsets[[i + 1]]],
next = 0
];
(*Append subset to vector*)
MyVect = Append[MyVect, SortSetClockwise[subset]];
(*Append subsets to matrix*)
If[next != Length[subset] && Length[MyVect] > 0,
MyMat = Append[MyMat, {curr, MyVect}];
MyVect = {};
];
}];
(*Return*)
{roots, MyMat}
]
The purpose of this step is to classify solutions by
If we put all of this together, we can write the following function:
ReduceCentrifugeSols[n_] := Module[{Sols, SubSols, UniqueSubSols, UniqueSols, MyLength, permuteSolsBool, RotatedSol, RotatedSols, Sol, Add, element, i},
Sols = ClassifyCentrifugeSols[n];
MyLength = Length[Sols[[2]]];
UniqueSols = {};
(*Iterate over each subset size*)
For[i = 1, i <= MyLength, i++,
UniqueSubSols = {};
SubSols = Sols[[2]][[i]][[2]];
(*Loop over solution sub-space*)
For[element = 1, element <= Length[SubSols], element++,
RotatedSols = {};
Sol = SubSols[[element]];
Sol = SortSetClockwise[Sol];
(*Rotate one solution*)
For[k = 0, k < n, k++,
RotatedSol = RotateSol[n, k, Sol];
RotatedSol = SortSetClockwise[RotatedSol];
RotatedSols = Append[RotatedSols, RotatedSol]
];
(*Add to list of unique solutions*)
If[Length[Intersection[UniqueSubSols, RotatedSols]] == 0,
UniqueSubSols = Append[UniqueSubSols, Sol];
]
];
UniqueSols = Append[UniqueSols, {Sols[[2]][[i]][[1]], UniqueSubSols}];
];
(*Return*)
{Sols[[1]], UniqueSols}
]
Likewise, I wrote DrawCentrifugeUniqueSols
to draw the solutions to ReduceCentrifugeSols
:
As we can see, this step of reducing solutions by rotating and comparing them to each other removed {
This improvement becomes super clear when we solve for
Going further
Improvement A
One minor issue I've noticed with my ReduceCentrifugeSols
method is that it doesn't reduce solutions that are the same under reflections (for instance,
Improvement B
Another point I'd like to work on in the future is to rewrite the code to use pipes (RightComposition
), so that functions can be properly composed:
or
Improvement C
Another issue with this method is that the computation time scales exponentially as
A solution is valid if and only if both the number of test tubes,
, and the number of empty spots, , can be expressed as sums of prime factors of
we would be able to remove the step of checking totals,
-
The full code can be found on https://github.com/aldomann/the-centrifuge-problem/. ↩