ETOOBUSY đ minimal blogging for the impatient
AoC 2016/19 - Halving Josephus
TL;DR
On with Advent of Code puzzle 19 from 2016: part 2 of the puzzle is a detour that I call halving Josephus.
I hope Iâm not giving too much out, but after solving part 1 of puzzle 19 (see AoC 2016/19 - Josephus problem) I was presented with the following part 2:
Realizing the folly of their present-exchange rules, the Elves agree to instead steal presents from the Elf directly across the circle. If two Elves are across the circle, the one on the left (from the perspective of the stealer) is stolen from. The other rules remain unchanged: Elves with no presents are removed from the circle entirely, and the other elves move in slightly to keep the circle evenly spaced.
Itâs an interesting twist because itâs different from the generalizations of taking every $n$-th item (the default case being taking every second item) and has this interesting even/odd differentiation.
Some brute force can help
As Iâm requested to solve the puzzle with $3014603$ elves⌠going the brute force way is doable although not fast at all.
As a matter of fact, hereâs what happened to me: I started a brute force simulation, let it run and in the meantime worked on a more efficient solution. Which arrived before the one from the brute force approach đ
So is the brute force approach useful at all! Yes it does! It is reasonable fast when run on smaller inputs, so it can help in looking at the data and trying to spot any regularity that we can exploit to solve the problem. Even if we donât prove that the regularity is going to apply for whatever input⌠we can still try to code it, get a result and cross fingers đ
Here is a sub implementing the brute force approach for this problem:
sub josephus_bf ($n) {
my @slots = 1 .. $n;
while ((my $N = @slots) > 1) {
my $opponent = $N % 1 ? ($N - 1) / 2 : $N / 2;
splice @slots, $opponent, 1;
push @slots, shift @slots;
}
return $slots[0];
}
It implements the problem almost literally:
- initialize with all applicable elves;
- the âcurrent elfâ is always the first one in the
@slots
array (push @slots, shift @slots
makes sure to move it at the end of the array after its turn, so that its successor will be the first item in the next iteration) - until thereâs more than one elf with a present, find out the index of
the mid-placed elf (getting the lowered-number elf for an odd number
of elves in
@slots
) and remove it (usingsplice
).
Looking at some data
Letâs first get a look at some data for low numbers:
11 -> 2 21 -> 15
2 -> 1 12 -> 3 22 -> 17
3 -> 3 13 -> 4 23 -> 19
4 -> 1 14 -> 5 24 -> 21
5 -> 2 15 -> 6 25 -> 23
6 -> 3 16 -> 7 26 -> 25
7 -> 5 17 -> 8 27 -> 27
8 -> 7 18 -> 9 28 -> 1
9 -> 9 19 -> 11 29 -> 2
10 -> 1 20 -> 13 30 -> 3
There are a couple of interesting patterns:
-
the âwinnerâ elf slot (starting from
1
) tends to increase until it reaches a point where it is reset. This is similar to what happens with the traditional Josephus problem; -
the reset seems to happen immediately after a power of $3$, and also powers of $3$ seem to have that the last elf is also the winner (e.g. see the values for $3$, $9$, and $27$);
-
the increasing goes by one unit up to a point, then it goes by two units (apparently). It seems to go by one up to the double of the last power of $3$, in particular.
At this point we can leverage the brute force function again and check what happens around other powers of $3$:
3^4 = 81 3^5 = 243 3^6 = 729
78 -> 75 240 -> 237 726 -> 723
79 -> 77 241 -> 239 727 -> 725
80 -> 79 242 -> 241 728 -> 727
81 -> 81 243 -> 243 729 -> 729
82 -> 1 244 -> 1 730 -> 1
83 -> 2 245 -> 2 731 -> 2
84 -> 3 246 -> 3 732 -> 3
and around their doubles:
2*3^4 = 162 2*3^5 = 486 2*3^6 = 1458
159 -> 78 483 -> 240 1455 -> 726
160 -> 79 484 -> 241 1456 -> 727
161 -> 80 485 -> 242 1457 -> 728
162 -> 81 486 -> 243 1458 -> 729
163 -> 83 487 -> 245 1459 -> 731
164 -> 85 488 -> 247 1460 -> 733
165 -> 87 489 -> 249 1461 -> 735
OK! From a mathematicianâs point of view these are fair clues that cry for some demonstration. From an engineerâs point of view⌠itâs time to code.
Letâs call it a heuristicâŚ
⌠for lack of any formal demonstration.
The basic idea is the following:
- first, letâs find the power of $3$ that is immediately lower than, or equal to, our number.
- if itâs equal weâre done: our small investigation suggests that returning the input number itself is a good guess;
- otherwise, we have to understand where we lie between that power of
$3$ and the one immediately following and, depending on the position,
go ahead:
- if in the first half, the increase is one by one;
- from the start of the second half on, the increase is two by two.
This is the code:
1 sub josephus_ternary ($n) {
2 my $u3 = 3 ** int(log($n) / log(3));
3 return $n if $n == $u3;
4 my $threshold = int($u3 * 2);
5 return $n - $u3 if $n <= $threshold;
6 return ($n - $u3) + ($n - $threshold);
7 }
Variable $u3$ in line 2 is initialized to the power of $3$ we are after. the ratio of the two logarithms is a way of calculating the logarithm in base $3$; we take its integer part and the use it as an exponent for $3$ and voilĂ we have a power of $3$.
As promised, if this power of $3$ is equal to our input number⌠we know pretty well that the last elf is the one that gets it all. Line 3 puts this in code.
Line 4 finds out the threshold between the increment by one and the
increment by two parts. Then we return the right value (lines 5 and 6)
depending on where $n
lies with respect to this threshold.
And it works!
As anticipated, while the brute force algorithm was still running with
an input of 3014603
, I was able to use josephus_ternary
and get the
right answer: 1420280
.
Now you can, too.