TL;DR

The Advent of Code puzzle 15 from 2016 has more Chinese Remainder Theorem!

As you might have noticed, I’ve been taking a look at the 2016 edition of the puzzles in Advent of Code.

No, this will not be another series!

It so happens that puzzle 15 has a long and involved description about capsules, discs, alignments, exact timings… I had to read it twice and put my brain in full imaginative mode.

Anyway, as I read through it, I started suspecting that it had to do with the Chinese Remainder Theorem. Which, indeed, it does.

First thought was: again?!? Then I had that Back to the Future moment when I realized that this puzzle came before the ones I discussed last December 🙄

Second thought was: I don’t want to understand this problem, I want to COOOODE!.

He who its first…

… hits twice, right?

So I went ahead, took the relevant functions from cglib’s Numbers.pm, put some parsing, a bit of this, a bit of that… and ended up with the following:

#!/usr/bin/env perl
use 5.024;
use warnings;
use English qw< -no_match_vars >;
use autodie;
use experimental qw< postderef signatures >;
no warnings qw< experimental::postderef experimental::signatures >;
use File::Basename qw< basename >;
use Data::Dumper; $Data::Dumper::Indent = 1;
use Storable 'dclone';
$|++;

my @stuff;

my $filename = shift || basename(__FILE__) =~ s{\.pl\z}{.tmp}rmxs;
open my $fh, '<', $filename;
while (<$fh>) {
   my ($delay, $n, $position) = m{
      \A Disc \s+ \#(\d+) \s+
      has \s+ (\d+) \s+ positions .*?
      at \s+ position \s+ (\d+)
   }mxs or die $_;
   push @stuff, $n, ($delay + $position) % $n;
}
close $fh;

say((chinese_remainder_theorem(@stuff))[1]);

# chinese_remainder_theorem and egcd below... nothing new

I have to admit that I was a bit unsure about the ($delay + $position) % $n - it was somehow a shot in the dark.

Anyway, I run it over the example input and presto! - it worked! Right off the bat!

$ cat 15.tmp 
Disc #1 has 5 positions; at time=0, it is at position 4.
Disc #2 has 2 positions; at time=0, it is at position 1.

$ perl 15-1.pl 15.tmp
5

OK, on with my puzzle input then:

$ cat 15.input 
Disc #1 has 13 positions; at time=0, it is at position 1.
Disc #2 has 19 positions; at time=0, it is at position 10.
Disc #3 has 3 positions; at time=0, it is at position 2.
Disc #4 has 7 positions; at time=0, it is at position 1.
Disc #5 has 5 positions; at time=0, it is at position 3.
Disc #6 has 17 positions; at time=0, it is at position 5.

$ perl 15-1.pl 15.input
64118

Only that… NO, it does not work!!!

I hit first… but I hit wrong!

Back to the paper

This must be my humbling year, because I’m reminded so many times of how many ways I have to fail!

Well, I meant learn 👨‍🎓

Let’s see… if we start at time $T$, the first disk is reached after a delay of $1$ at time $T + 1$, and if its starting position (at $t = 0$) is $P_{1, 0}$ then its position at $T + 1$ will be $T + 1 + p_1 \pmod {n_1}$. We have similar relations for the other discs:

\[P_{1, T + 1} \equiv T + 1 + P_{1, 0} \pmod {n_1} \\ P_{2, T + 2} \equiv T + 2 + P_{2, 0} \pmod {n_2} \\ ... \\ P_{i, T + i} \equiv T + i + P_{i, 0} \pmod {n_i}\]

If we really need that capsule, each of the left-hand sides MUST be $0$, which brings us to:

\[T \equiv -1 - P_{1, 0} \pmod {n_1} \\ T \equiv -2 - P_{2, 0} \pmod {n_2} \\ ... \\ T \equiv -i - P_{i, 0} \pmod {n_i}\]

This can also be rewritten as:

\[r_1 = T \pmod {n_1} = n_1 - (1 + P_{1, 0} \pmod {n_1}) \\ r_2 = T \pmod {n_2} = n_2 - (2 + P_{2, 0} \pmod {n_2}) \\ ... \\ r_i = T \pmod {n_i} = n_i - (i + P_{i, 0} \pmod {n_i})\]

I knew it!

It’s also funny that it worked for the example input: simply put, $1 \equiv -1 \pmod 2$, so the sign flip didn’t matter!

So the right code is actually this… a small change for a program, but a big step ahead for a puzzle solver!

#!/usr/bin/env perl
use 5.024;
use warnings;
use English qw< -no_match_vars >;
use autodie;
use experimental qw< postderef signatures >;
no warnings qw< experimental::postderef experimental::signatures >;
use File::Basename qw< basename >;
use Data::Dumper; $Data::Dumper::Indent = 1;
use Storable 'dclone';
$|++;

my @stuff;

my $filename = shift || basename(__FILE__) =~ s{\.pl\z}{.tmp}rmxs;
open my $fh, '<', $filename;
while (<$fh>) {
   my ($delay, $n, $position) = m{
      \A Disc \s+ \#(\d+) \s+
      has \s+ (\d+) \s+ positions .*?
      at \s+ position \s+ (\d+)
   }mxs or die $_;
   push @stuff, $n, $n - ($delay + $position) % $n;
}
close $fh;

say((chinese_remainder_theorem(@stuff))[1]);

# ...

Let’s run it…

$ perl 15.pl 15.input 
376777

Yay, this is correct now!

A final thought

I work in the telecommunications industry and most of my… coding occasions come in relation to relatively small integrations, so I definitely have a biased view.

This said… I wonder if the Chineses discovered this theorem just for the fun of puzzle builders and solvers in the 21st century!