Advent of code 2020, week 2
Published on December 14, 2020
Table of contents
Advent of code 2020 is ongoing. I’ve been solving
the problems daily using rust. So far so good. I have a private leaderboard
with a few folks, anyone is free to join, here’s the code: 9779-59e7cb46
.
I’m publishing my solutions here, with a delay of a day or two: https://github.com/hugopeixoto/aoc2020
Here are my personal stats so far:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-------Part 1-------- -------Part 2--------
Day Time Rank Score Time Rank Score
14 00:10:17 343 0 00:33:07 779 0
13 02:25:27 9424 0 03:26:30 4825 0
12 01:26:32 7219 0 01:39:25 5711 0
11 01:13:35 5964 0 01:20:22 4033 0
10 00:52:14 9128 0 01:01:41 3367 0
9 00:22:35 5624 0 00:34:57 4854 0
8 00:49:35 8777 0 00:56:21 6027 0
7 00:17:27 801 0 00:30:15 1022 0
6 00:08:04 2745 0 00:13:41 2145 0
5 00:23:43 4339 0 00:28:12 3544 0
4 00:20:52 4184 0 00:56:14 3896 0
3 00:56:04 8977 0 01:04:08 8165 0
2 00:07:32 1519 0 00:10:52 1224 0
1 01:10:16 6419 0 01:16:28 6046 0
Day 9 and day 14 were the only ones where I was awake at release time, but I’m still solving them same day, which was my main goal. This week had some harder problems. Day 10 and 13 stumped some folks. Here’s what I found interesting so far each day:
Day 8
I initially brute-forced part 2, but after submitting the solution I optimzed
it with a node marking depth first search. To make sure that it was indeed
faster, I started using cargo bench
to compare the running time of my
solutions.
I tried using scan_fmt
instead of
regular expressions when parsing the input file.
Day 9
The first part was similar to the 2SUM problem but with a rolling window kind
of thing. I used itertools::minmax
in the second part. It’s a bit clunky,
though:
1
2
3
4
if let MinMaxResult::MinMax(x, y) = numbers[lower..i].iter().minmax() {
println!("{}", x + y);
break;
}
Day 10
I solved part 2 using dynamic programming. At this point I started preallocating vectors, and it would be nice to have something shorter than this:
1
2
let mut ways: Vec<u64> = Vec::new();
ways.resize(numbers.len(), 0);
I could probably squeeze this to use constant memory with a small circular buffer.
Day 11
This is a variation of game of life. Initially I had a function that received a
board state and returned the new game state, but I changed it to something
similar to the double buffering technique so that memory isn’t constantly being
allocated, using mutable references to mutable vectors and the
destructuring_assignment
nightly feature:
1
2
3
4
5
6
7
8
9
10
11
12
13
#![feature(destructuring_assignment)]
fn next(a: &Vec<bool>, b: &mut Vec<bool>, neighbors: &Vec<usize>, thresh: usize) -> bool;
fn main() {
// ...
let mut a = &mut compact_state.clone();
let mut b = &mut compact_state.clone();
while next(a, b, &neighbors, 5) {
(a, b) = (b, a);
}
}
Day 12
Just a bunch of match
statements, nothing particularly interesting.
Day 13
This one required some maths, so it took me about an hour to get the second part solved. Nothing fancy on the rust side.
Day 14
I brute forced part 2, and I have no idea if there’s a smarter way. I learned a bit about passing slices around:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn gen2(mask: &[char], addr: u64, x: u64, results: &mut Vec<u64>) {
if mask.len() == 0 {
results.push(addr);
} else {
match mask[0] {
// ...
}
}
}
fn gen(mask: &Vec<char>, addr: u64) -> Vec<u64> {
let mut r = vec![];
gen2(&mask, addr, 0, &mut r);
r
}
Benchmark results
I’m benchmarking the main
functions directly, including the input file I/O,
using cargo bench
. Here are the current results:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
test bench_day01 ... bench: 33,130 ns/iter (+/- 9,334)
test bench_day02 ... bench: 106,041 ns/iter (+/- 45,522)
test bench_day03 ... bench: 36,203 ns/iter (+/- 9,872)
test bench_day04 ... bench: 1,602,220 ns/iter (+/- 214,088)
test bench_day05 ... bench: 131,899 ns/iter (+/- 42,727)
test bench_day06 ... bench: 618,439 ns/iter (+/- 244,812)
test bench_day07 ... bench: 2,163,174 ns/iter (+/- 410,470)
test bench_day08 ... bench: 321,834 ns/iter (+/- 51,744)
test bench_day09 ... bench: 123,836 ns/iter (+/- 10,562)
test bench_day10 ... bench: 8,940 ns/iter (+/- 4,849)
test bench_day11 ... bench: 7,497,058 ns/iter (+/- 748,109)
test bench_day12 ... bench: 22,708 ns/iter (+/- 8,289)
test bench_day13 ... bench: 7,517 ns/iter (+/- 1,562)
test bench_day14 ... bench: 8,969,778 ns/iter (+/- 770,472)
I was trying to squeeze them to fit under a millisecond each, but I’m having trouble with some of them. Day 4 and day 7 use regular expressions, and while I could remove them, the code complexity would sky rocket. Day 11 is the game of life variant, and I don’t see how I can improve it. I haven’t spent much time optimizing day 14, but it would probably require a non bruteforce approach.
I kind of went all-in on day 2, and parsed everything manually to avoid going through each character in the input file more than once. I learned that I could use a match statement like this:
1
2
3
4
5
6
7
8
let state = 0;
for character in input.chars() {
match (character, state) {
('\n', _) => { /* ... */ },
('0'..='9', 1) => { /* ... */ },
('0'..='9', _) => { /* ... */ },
}
}
Bonus - Project euler
I got carried away and solved five more project euler problems: 121, 122, 123, 125, and 126. I enjoyed solving 126 and 122. I got stuck on 127.