Spoilers ahead for Advent of Code 2024 solutions.
intro
I'm not quite sure how to start this.
I've been quiet on this blog for a while, simply because I've been too tired to do anything meaningful whatsoever (maybe "tired" isn't the right word, tiredness implies there's will somewhere behind it all). I'm lagging behind in so many aspects of my life1 and just generally have been, to put it simply, a pile of crap.
Putting that all aside though, it's that time of year again...when computer nerds around the world rejoice because Advent is upon us (no not that one), and what better way to celebrate than puzzles!
Joyful, marvellous puzzles!
Advent of Code, if you didn't already know about it, is a yearly set of programming puzzles that are all holiday-themed—usually you're an elf trying to help find Santa or deliver presents or something. Each puzzle has two parts which drop at midnight, with the second part being a spin on the first and only being unlocked once you finish said first. Each part gives a star once you've solved it.
25 days, 50 stars. Got it so far?
Now you don't have to solve the puzzles with programming, you could, for instance, do it in Factorio, or with Excel, or something completely ridiculous. But most use it as a nice exercise to break in a language they're learning, or one they love, or just for the fun of it (and going super fast!).
I'd really have liked to use something new this year, perhaps Clojure, OCaml, or Gleam, but I didn't prepare in time and I'd really not want my first experiences with any of those languages to be one of frustration because I couldn't get past a particular day's problems. So Rust it is, because currently, that's the language I'm most comfortable with right now.2
(For comparison, some people I know are using Go, Unison, Zig, and Clojure, so they're certainly a diverse bunch)
And well, I have a blog, don't I? So instead of whining about this on a silly little group some of my friends and I are in, I'll post all my thoughts about every day's problems here—it's not like anyone's going to see it anyways, I'm not going to link to it and stuff.
Last year I only got up to day 9 (and day 8 was a doozy3), so lets see how I fare this year, shall we?
(Also, the folks over at the subreddit are really nice, and I'm definitely going to be snooping around and looking at solutions when I run into a wall, so thanks to them!)
day 1
Now, even though the puzzles drop at 6am my time, I only managed to get a look at today's one by around 1pm, and it looked...quite simple actually.
We've got two lists of numbers, and my intuition is to simply:
- Go ever every line in the input and,
- Collect the numbers into two lists, then,
- Sort the lists, iterate through them in pairs,
- Sum up the differences.
Pretty straightforward Rust4:
let (mut l, mut r): (Vec<_>, Vec<_>) = input .lines() .map(|l| l.split_once(" ").unwrap()) .map(|(a, b)| (a.parse::<u32>().unwrap(), b.parse::<u32>().unwrap())) .collect(); l.sort_unstable(); r.sort_unstable(); l.iter().zip(r).map(|(x, y)| x.abs_diff(y)).sum()
...along with a test:
#[test] fn test_part_one() { let input = "\ 3 4 4 3 2 5 1 3 3 9 3 3 "; assert_eq!(part_one(input), 11); }
$ cargo test ... running 1 test test days::_1::test_part_one ... ok
And we get our answer!
Part two switches things up by having us check the number of occurrences a number in the left list occurs in the right list. The first thing that comes to my head, of course, is "just use a hashmap!!!!" haha.
Instead of collecting the left and right columns into two sorted lists, we simply throw the left one into a list and the right into a map of "occurrences" of that number. Then we run through the first list and sum up the lookups in the map.
The itertools crate provides a counts()
method that returns a frequency map for a iterator, so I'll just pull that in and use it instead of implementing my own:
use itertools::Itertools; let (l, r): (Vec<_>, Vec<_>) = input .lines() .map(|l| l.split_once(" ").unwrap()) .collect(); let r = r.iter().counts(); l.iter() .map(|x| r.get(x).map_or(0, |y| x.parse::<usize>().unwrap() * y)) .sum()
I got rid of the mut
for the vectors since we don't need it anymore, and moved the number conversion into the second iteration5.
We test:
$ cargo test Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s Running unittests src/main.rs (target/debug/deps/adventurous-0000804102b85ed7) running 2 tests test days::_1::test_part_one ... ok test days::_1::test_part_two ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
We run...and correct!
Gold star x2.
In my post-solve sub browse, I found an appropriate meme by u/mothlordmilk:
day 2
Slept around 3am because I was hanging out with someone, slept through my 6am alarm for like 3 hours (it had stopped playing and was just vibrating at that point), and then spent a couple more hours investigating why google.com wasn't working. 6 7
Anyways, puzzle time.
Okay so we need to run through every line and make sure they're increasing by a certain amount...part of me thinks that i should parallelise this to make it "faster", but we'll deal with that later.
Since we're filtering out the lines that aren't "right", we can stub out the main thing like so:
input .lines() .filter(|l| {}) // ← how to *actually* filter? .count()
Now in each line after getting the numbers, we need to know if it's meant to be "increasing" or "decreasing", before we start to check the differences...if we had an array of numbers then obviously the best approach would be:
if arr[0] - arr[1] > 0 { // decreasing } else { // increasing }
But then the thing returned by the split is an iterator, and I don't want to have to collect it first. Maybe if we can make something that holds the increasing/decreasing value, but that can only be "touched" once, so that only the first two elements can set it?
Originally I went with an optional boolean that'd describe whether it's increasing or decreasing, but then Rust has the concept of Ordering
s, so let's use that:
... .filter(|l| { let mut ordering = None; let mut nums = l.split(' ').map(|x| x.parse::<u8>().unwrap()); let first = nums.next().unwrap(); for n in nums { // initialise `ordering` ordering = ordering.or_else(|| Some(first.cmp(&n))); if ordering == Some(std::cmp::Ordering::Equal) { return false; } // ← still needs processing... } true })
ordering
starts out as None
, and we make the iterator of numbers and grab the first value (there'll always be a first, so unwrap), then go over it:
-
If
ordering
isNone
calculate it with whatever value we're looking at now, else leave it. -
If we calculated it and it's
Equal
, then we've hit an edge-case and might as well exit early, it's already invalid.
Personally, I think dealing with the more unlikely cases first makes sense here, as you can just short-circuit and avoid checking more things.
Filling out the rest:
... for n in nums { ... if ordering != Some(first.cmp(&n)) { return false; } if first.abs_diff(n) > 3 { return false; } first = n; } true ...
We check to make sure the new ordering
calulated matches up with the original one, and that the difference between the two numbers is not more than 3, then reuse the first
variable, which really should've been named prev
in hindsight.
Spit out a test, and it passes, giving 2 as the answer for the sample.
But since we already have itertools...
for (c, n) in nums.tuple_windows() { ordering = ordering.or_else(|| Some(p.cmp(&n))); if ordering == Some(std::cmp::Ordering::Equal) { return false; } if ordering != Some(p.cmp(&n)) { return false; } if p.abs_diff(n) > 3 { return false; } }
...where tuple_windows
returns an iterator over "windows" into the container (e.g. calling it on [1, 2, 3, 4]
gives (1, 2), (2, 3), (3, 4)
.
The Problem Dampener in part 2 shakes things up a bit, making us have to tolerate one bad level in the report.
First intuition is to have a boolean called "is_dampened" and turn it on if we see something bad instead of outright returning false
to the .filter()
. But this runs into a problem: working through 1 2 7 8 9
, our approach would go "okay it's increasing", then (1, 7)
which gets dampened, then (7, 8)
and (7, 9)
, and succeeds, even though removing neither 2
nor 7
works.
One of the examples is 8 6 4 4 1
, and maybe instead of looking at two elements at a time, we consider three (the previous, current, and the next):
- First we try
8 6
and setordering
to decreasing, - Then we try
6 4
, that's also fine, - We try
4 4
, which is bad, but then also consider the previous one (6
) which works, so we dampen the first4
, - Then
4 1
, which is okay.
The only potential problem I can think of is changing ordering
after it has already been set, if the second level needs to be the dampened, e.g. in 3 1 4 5 6
, it'd go "okay it's decreasing", then (1, 4)
which gets dampened, and then (3, 4)
and fail because it's increasing, even though removing 1
works.
Maybe the inputs don't have that kind of data? Let's add that example to the test cases and check:
input .lines() .filter(|l| { let mut ordering = None; let nums = l.split(' ').map(|x| x.parse::<u8>().unwrap()); let mut is_dampened = false; let mut prev = 0; for (curr, next) in nums.tuple_windows() { ordering = ordering.or_else(|| Some(curr.cmp(&next))); if ordering == Some(std::cmp::Ordering::Equal) || ordering != Some(curr.cmp(&next)) || curr.abs_diff(next) > 3 { if is_dampened { return false; } // try with prev and next instead ordering = ordering.or_else(|| Some(prev.cmp(&next))); if ordering == Some(std::cmp::Ordering::Equal) || ordering != Some(prev.cmp(&next)) || prev.abs_diff(next) > 3 { return false; } is_dampened = true; continue; } prev = curr; } true }) .count()
Totally inelegant.
And doesn't pass the sniff test, even though it passes the tests, the site says our answer is too low.
What if the first level is the one that has to be skipped, like 1, 6, 7, 8, 9
? prev
here gets initialised at 0, which would result in a fail.
// ↓ inside the first if statement { if is_dampened { return false; } if prev == 0 { // this is the first report prev = next; } else { // try with prev and next instead ordering = Some(prev.cmp(&next)); if ordering == Some(std::cmp::Ordering::Equal) || ordering != Some(prev.cmp(&next)) || prev.abs_diff(next) > 3 { return false; } } is_dampened = true; continue; }
Still no dice.
Okay, what if skipping the first element is harder, like 8 9 8 7 5
?
I solve that, still too low.
At this point, I decide to go to the subreddit and get some sample edge cases and throw them into tests...and I still can't crack it.
At 11pm-ish, I'm on a call with my friend where he talks about mapping the list of numbers to the differences between them...and while that works for part one, in part two the "ignore it if it's fixable" directive isn't easily dealt with, and...
Ah yes, a 10 minute wait before I can send my next answer.
I went to look at solutions.
Okay, so while an O(n) approach is totally doable, those pesky edge cases seem to be the major problem...most people seem to be going for recursion—when a bad report is hit, it (along with its neighbours) is just taken out, and the whole thing is tried again.
/u/lyon made a suggestion to use itertools::combinations
along with any()
to brute-force the solve—if it works with one element removed, it's fine, and if it doesn't then it just tries with another one removed, until every possibility is tried.
It's day 2, so the inputs are tiny, and the whole thing completes in less than 20ms according to time
:
$ time ./target/debug/adventurous 2 ... ./target/debug/adventurous 2 0.02s user 0.00s system 94% cpu 0.023 total
And that's even in debug mode!
Which is why I don't even end up checking out rayon anymore...there's not that much data, it'd just slow down things if anything.
input .lines() .map(|l| l.split(' ').map(|x| x.parse::<u8>().unwrap())) .filter(|nums| { nums.clone() .combinations(nums.clone().count() - 1) .any(|x| { let mut ordering = None; for (c, n) in x.iter().tuple_windows() { ordering = ordering.or_else(|| Some(c.cmp(n))); if ordering == Some(std::cmp::Ordering::Equal) || ordering != Some(c.cmp(n)) || c.abs_diff(*n) > 3 { return false; } } true }) }) .count()
It's currently 1:30am on the 3rd. Not a great start.
day 3
So the first thing that hits me here is that the input is HUUUGEEEEEE! Like wow you definitely need a computer for that.
Okay, we're just "isolating* mul(x,y)
instructions...so either a parser-combinator like nom
would suffice, or regex. And while parsing is arguably the "cleaner" and "better" and better-looking solution ("yes yes, just parse any expression in this form and throw away the mul
, just give me the numbers"), I'm going to go with a naïve regex, which I assume will work unless mul
s can be nested within each other, and for now that's a no.
Because you know what is the second most relevant piece of compsci advice after Just Use Hashmaps™? It's Just Use A Regular Expression™.8
VSCode's search actually has a regex function (it's the tiny third button in the search overlay that looks like a .*
, so let's just test it there as opposed to using (regexr.com/)[https://regexr.com/]—it's not terribly complicated yet.
- We want a literal
mul
, so we start out with just thatmul
, - Then a bracket, which needs to be escaped:
mul\(
, - Then a digit, but one or more of it (the example input only has 1-, but the actual input has 3-digit numbers):
mul\(d+
, - Then a comma and another set of digits:
mul\(d+,d+
, - Then we round it up with another bracket:
mul\(\d+,\d+\)
.
How do we do regex with Rust? Well there's the regex crate. We don't need the unicode
feature so let's just add it without that:
$ cargo add regex --no-default-features -F std,perf Updating crates.io index Adding regex v1.11.1 to dependencies ... $ tail -n1 Cargo.toml regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] }
And we make our thing...
let re = Regex::new(r"mul\(\d+,\d+\)").unwrap(); println!("{:?}", re.captures(input)); todo!()
...which fails to compile.
Wait what? \d
is Unicode?
$ cargo test
...
regex parse error:
mul\((\d+),(\d+)\)
^^
error: Unicode-aware Perl class not found (make sure the unicode-perl feature is enabled)
...
Okay lets add unicode-gencat
to the features, I'm not really sure how much performance I'm really saving now...
And it compiles!
$ cargo test -- -- show-output ... Some(Captures({0: 1..9/"mul(2,4)"})) ...
Since we only care about the numbers, lets put them in a named capturing group, which makes our regex look like "mul\((?<a>\d+),(?<b>\d+\))
now.
$ cargo test -- -- show-output ... Some(Captures({0: 1..9/"mul(2,4)", 1/"a": 5..6/"2", 2/"b": 7..9/"4)"}))
Nice, a
and b
correspond to the numbers. Now to iterate over all the captures and multiply and sum them up:
let re = Regex::new(r"mul\((?<a>\d+),(?<b>\d+)\)").unwrap(); input .lines() .map(|l| { re.captures_iter(l) .map(|c| c.extract().1.into()) .map(|(a, b)| a.parse::<u32>().unwrap() * b.parse::<u32>().unwrap()) .sum::<u32>() }) .sum()
Part 2 adds a couple of new instructions, do()
and don't()
, which affect how we compute the sum. Now we basically have to map every line of input to the multiplied numbers and the instructions, and instead of naïvely summing it, need to look at the do()
s and don't()
s. Nothing a simple fold can't do.
Let's make a container for the instructions so we can keep them all in a list:
enum Instruction { Mul(u32, u32), Do, Dont, }
Then beef up our regex to also look for do()
s and don't()
s while naming their capture groups:
mul\((?<a>\d+),(?<b>\d+)\)|(?<do>do\(\))|(?<dont>don't\(\))
Then map the captures to Instruction
s instead, using the capture group's existence to know which to use:
re.captures_iter(l) .map(|c| { c.name("a").map_or_else( || c.name("do").map_or(Instruction::Dont, |_| Instruction::Do), |x| { Instruction::Mul( x.as_str().parse::<u32>().unwrap() * c.name("b").unwrap().as_str().parse::<u32>().unwrap(), ) }, ) })
(We check if the a
group exists, and if it does, we spit out a Mul
with the value of the b
group, if it doesn't then check for the do
group, and do on.)
Then, we reduce the entire line of instructions to a singular number using fold()
:
.fold((true, 0), |(mut acc, mut sum), x| { match x { Instruction::Do => acc = true, Instruction::Dont => acc = false, Instruction::Mul(y) => { if acc { sum += y; } } } (acc, sum) })
Here the accumulator which is carried between the instructions consists of a sum
(that describes the sum so far), and a boolean acc
which tells us whether we should add the next Mul
instruction or not. Seeing a Do
or Dont
modifies acc
, and handing Mul
looks at it first to know whether it should actually add it.
In the end it looks like this:
let re = Regex::new(r"mul\((?<a>\d+),(?<b>\d+)\)|(?<do>do\(\))|(?<dont>don't\(\))").unwrap(); input .lines() .map(|l| { re.captures_iter(l) .map(|c| { c.name("a").map_or_else( || c.name("do").map_or(Instruction::Dont, |_| Instruction::Do), |x| { Instruction::Mul( x.as_str().parse::<u32>().unwrap() * c.name("b").unwrap().as_str().parse::<u32>().unwrap(), ) }, ) }) .fold((true, 0), |(mut acc, mut sum), x| { match x { Instruction::Do => acc = true, Instruction::Dont => acc = false, Instruction::Mul(y) => { if acc { sum += y; } } } (acc, sum) }) }) .map(|(_, x)| x) .sum()
...And the answer is tooooo high.
Now, this program here resets acc
to true every line, but the puzzle seems to treat the entire input as contiguous. So let's get rid of .lines()
and operate on the input itself, and:
... // ↑ enum and regex definition re.captures_iter(input) .map(|c| { c.name("a").map_or_else( || c.name("do").map_or(Instruction::Dont, |_| Instruction::Do), ... // ← rest... (acc, sum) }) .1
And yeah, got it!
I do wish my part 2 solutions didn't really end up "ruining" the straightforwardness of the part 1 ones...I'm just going to scroll around on the sub and see what others did.
...
Ah yes, one-liners in Python.
/u/PatolomaioFalagi posted the obligatory xkcd:
day 4
Grids.
Alright we need to look for a word in a grid, except they can all overlap. Naïvely, of course, I'm going:
- Walk through the grid line by line,
- On every occurrence of
X
, check its eight cardinal directions forMAS
, - If you see them, then store the beginning and end co-ordinates in a set.
I think that'll work, but I'm trying to think of more efficient ways to do it—maybe we can check every letter instead? No, that's "more" work since X
is the only way XMAS can start anyways.
I can't think of a Part 2 twist that I have to accommodate, so...welp let's do it then.
Summon a grid from the void:
struct Position { x: usize, y: usize, } struct Grid { occurrences: HashSet<(Position, Position)>, data: Vec<Vec<char>>, }
Make the new
and at
functions:
fn new(data: Vec<Vec<char>>) -> Self { Self { occurrences: HashSet::with_capacity(100), data, } } fn at(self, pos: Position) -> Option<char> { self.data.get(pos.y).and_then(|v| v.get(pos.x).copied()) }
Then let's consider going "left" from a Position
.
Since the x
in Position
is the column, it means we'd want to subtract 3 from Position.x
to get the indices for the elements to the left of it. But then what happens if there isn't enough space? Like, for example, if we were already at the leftmost edge with an x-value of 0?
0 - 4
isn't representable in a usize
, so it will fail:
$ cargo test
...
thread 'main' panicked at src/days/_4.rs:30:25:
attempt to subtract with overflow
So, we'll use usize::checked_sub
so it stops at 0:
fn check_left(&mut self, start: &Position) -> Option<()> { let end = Position { x: start.x, y: start.y.checked_sub(3)? }; }
We make the function return an Option
because we then get to use ?
on fallible operations...if we can't subtract 3 without underflowing, then might as well just stop.
Then we'll grab the subslice corresponding to those co-ordinates and return the end for the caller to use:
// ↓ still in `check_left`: self.data .get(start.y) .and_then(|r| r.get(end.x..=start.x)) .filter(|x| x == &['S', 'A', 'M', 'X']) .map(|_| Some(end))?
Now in a check_xmas
function we call check_left
and if we get something, we throw it into occurrences
:
fn check_xmas(&mut self, start: Position) { self.check_left(&start).map(|e| self.occurrences.insert((start, e))); }
We should probably move the insertion into occurrences
into the check_left
function, since we're always going to call it:
fn check_left(&mut self, start: &Position) -> Option<()> { let end = Position { x: start.x, y: start.y.checked_sub(3)? }; self.data .get(start.y) .and_then(|r| r.get(end.x..=start.x)) .filter(|x| x == &['S', 'A', 'M', 'X']) .map(|_| { self.occurrences.insert((*start, end)); // ← added }) } fn search_cardinals(&mut self, start: Position) { self.check_left(&start); // ← shortened }
We're not doing anything with the result, so Option<()>
suffices.
I began to write check_right
and noticed I would be rewriting a lot of logic and only changing tiny bits, so I decided to stop and try and consolidate everything into a function. That involved making a tiny Direction
enum:
#[derive(PartialEq, Eq, Hash)] enum Direction { Right, Left, }
And then since the only thing that changes based on the Direction
is how we get the range of positions along it, we can match against that in check_xmas
:
fn check_xmas(&mut self, start: &Position, dir: Direction) -> Option<bool> { use Direction::*; let mut letters = "XMAS".chars(); let positions: Box<dyn Iterator<Item = Position>> = match dir { Right => Box::new((start.x..=start.x + 3).map(|x| Position { x, y: start.y })), Left => Box::new( (start.x.checked_sub(3)?..=start.x) .rev() .map(|x| Position { x, y: start.y }), ), }; for p in positions { if letters.next() != self.at(p) { return None; } } Some(self.occurrences.insert((*start, dir))) }
We're generating the positions
by mapping the range of valid values to the actual Position
(for Direction::Left
, we reverse the range so that the right-most one comes first).
Then we just run through the iterator and make sure it yields X
, M
, A
, and S
.
(Since we have the bool from inserting into occurrences
we might as well return it, even though we're not doing anything with it).
Just to make sure, let's throw some tests in:
#[test] fn part_one_minis() { assert_eq!( Grid::new(vec![vec!['X', 'M', 'A', 'S']]) .check_xmas(&Position { x: 0, y: 0 }, Direction::Right), Some(true) ); assert_eq!( Grid::new(vec![vec!['S', 'A', 'M', 'X']]) .check_xmas(&Position { x: 3, y: 0 }, Direction::Left), Some(true) ); }
Which pass, good.
Now, let's do upwards:
// ↑ in the `match` statement Up => Box::new( (start.y.checked_sub(3)?..=start.y) .rev() .map(|y| Position { x: start.x, y }), ), ... // ↓ in `part_one_minis()` assert_eq!( Grid::new(vec![vec!['S'], vec!['A'], vec!['M'], vec!['X']]) .check_xmas(&Position { x: 0, y: 3 }, Direction::Up), Some(true) );
And downwards:
Down => Box::new((start.y..=start.y + 3).map(|y| Position { x: start.x, y })), ... assert_eq!( Grid::new(vec![vec!['X'], vec!['M'], vec!['A'], vec!['S']]) .check_xmas(&Position { x: 0, y: 0 }, Direction::Down), Some(true) );
Down-right involves zipping up two iterators for both directions:
DownRight => Box::new( (start.x..=start.x + 3) .zip(start.y..=start.y + 3) .map(|(x, y)| Position { x, y }), ), ... assert_eq!( Grid::new(vec![ vec!['X'], vec!['.', 'M'], vec!['.', '.', 'A'], vec!['.', '.', '.', 'S'] ]) .check_xmas(&Position { x: 0, y: 0 }, Direction::DownRight), Some(true) );
And I'll skip the rest of the diagonals here because you get the gist.
Then we just parse the input and do the check when we're on an X
:
// ↑ in `impl Grid` fn count_xmas(&mut self) { use Direction::*; for x in 0..self.data[0].len() { for y in 0..self.data.len() { let pos = Position { x, y }; if self.at(pos) == Some('X') { for d in [Right, DownRight, Down, DownLeft, Left, UpLeft, Up, UpRight] { self.check_xmas(&pos, d); } } } } } ... // ↓ in `main()` let mut grid = Grid::new(input.lines().map(|l| l.chars().collect_vec()).collect_vec()); grid.count_xmas(); grid.get_xmas_count()
Which passes!
For part 2, we're looking in an X-shape, but since the A
at the middle is unique, let's get rid of the directions and check when we encounter one:
let dr = self.at(Position { x: start.x + 1, y: start.y + 1, })?; let ur = self.at(Position { x: start.x + 1, y: start.y.checked_sub(1)?, })?; let dl = self.at(Position { x: start.x.checked_sub(1)?, y: start.y + 1, })?; let ul = self.at(Position { x: start.x.checked_sub(1)?, y: start.y.checked_sub(1)?, })?; if (dr, ur, dl, ul) == ('S', 'S', 'M', 'M') { Some(self.mas.insert(*start)) } else { None } ... for x in 0..self.data[0].len() { for y in 0..self.data.len() { let pos = Position { x, y }; if self.at(pos) == Some('A') { self.check_mas(&pos); } } }
This fails the test, and hm...it seems the letters can rotate around the A
, but two M
s and S
s will always be on a side right?
There's probably a better way to do all this, but let's just list all the valid cases:
let valid = [ ('S', 'S', 'M', 'M'), ('M', 'M', 'S', 'S'), ('M', 'S', 'M', 'S'), ('S', 'M', 'S', 'M'), ]; if valid.contains(&(dr, ur, dl, ul)) { Some(self.mas.insert(*start)) } else { None }
And...we pass!
I should probably start reading up on graph traversal algorithms because I feel a maze is imminent any day now...
day 5
I'm starting day 5 pretty late (it's around 9pm here), partly because I had a test of sorts earlier today, and partly9 because I'm lazy (amongst other things). A friend told me today that when they took a mental health break from everyone recently, it was actually mostly from me, and well, when someone tells you that...
Also, my SSD appears to be failing because my filesystem constantly becomes read-only.
Fun!
TODO: pic
Anyways, puzzles!
I'm not even sure what to call this kind of thing, is it some sort of logic thing? A search-but-only-kinda? I mean we do appear to build up a table of rules and then make sure the data adheres to them (and then getting the middle number for no reason), but I...well, words fail me.
Can I get some sort of ASCII diagram in here please?
Okay, so the first part of the input just says "here's x
and y
, if they both occur in the data, x
has to come before y
". Simple enough, just hashmap x
to a bunch of y
s. But how do we check it?
The first idea would be to go through the line of data left-to-right and be all "okay here's a number, what's allowed to come after it", and then looking for it in the line...but that wouldn't be able to detect if y
comes before x
right?
Maybe we can go...backwards?
UNFINISHED
-
totally haven't started applying for my masters, for one... ↩
-
I'd like to be able to do it with the language I'm making at a point though, but that's probably never going to happen. ↩
-
c'mon, how do you expect me to know the Chinese Remainder Theorem in order to optimise a problem so it doesn't take forever to run?? ↩
-
unfortunately, mataroa's (this blog platform) code highlighting could be better... ↩
-
I personally don't like converting things to
usize
since it's technically meant for pointer locations, but in this case I'm too tired to be pedantic sinceItertools::counts
returns aHashMap<T, usize>
and I can't change that. ↩ -
I think it's because the IPv4 address wasn't being spat out and my network obviously doesn't know what IPv6 is, sigh...but then I'm using 1.1.1.1 which is supposed to be the best of them all. It's not consistent, sometimes the IPv4 address is returned, but I suppose switching to 8.8.8.8 helps sorta? ↩
-
made a couple of tweets, 1.1.1.1 appears to be timing out while 1.0.0.1 is working fine...why? I'm not sure. ↩
-
the third being, of course, Just Use Postgres™. ↩
-
read: mostly. ↩