Advent of Code 2024 Week 1

Christof Reimers

2311 words, 12 mins

Here we are, once again. Advent of Code is back at it again and I'm taking part. Last year I stopped after 11 days and didn't feel like writing a post about it. But this year I changed a few things.

Preamble

This years Advent of Code started with me not starting it. I did not do Day 1 on December 1st. But in the evening on December 2nd I eventually came around and did it. I was a bit in a hurry and slapped something together in the TypeScript Playground, meaning no fancy reading the input, no fancy tests, no fancy nothing, not even git (which is madness).

This shitty approach has now become somewhat my way of doing things this year: a little bit shit and sloppy. But I wouldn't say the code is shit, I would say it's (at least a little bit) ✨art✨.

In this post I will showcase my solutions and say something (probably not much) about each solution.

Self imposed rules

Over the past week a specific way of doing things revealed itself, again and again: Writing everything in one line.

Some might think: Isn't that not unreadable and easy? Just remove all the linebreaks...

But that is not the route I took. I talking more about a "logical" line. Meaning no place where I would usually place a semicolon or a curly bracket. This comes with two kinds of problems.

Functions

Functions have an obvious solution: Lambads. If I don't reuse that piece of code I also don't use a function. Makes the code less readable, but that is somewhat the point.

123
function x(param) {
<some code>
}

becomes

1
const x = param => <some code>;

Well, something isn't looking right there.

Assignments

Assignments without creating a new line are impossible. Is what I would have said a week ago. Then I stumbled across IIFEs.

12
const x = <some value>;
<some code using x>

becomes

1
(x => <some code using x>)(<some value>)

While it reverses the read order of things, it eliminates the need for a linebreak.

Array methods

One thing I always find interesting is how "easy" Advent of Code solutions are to write using the methods of JavaScript arrays. That specifically means map, reduce, filter, some and so on. These combined with lambdas and IIFEs should result in the perfect toolset to write unreadable one liners with horrible performance. Excuse me, I meant, to write ✨art✨.

The only exception to this is a field called input which contains a long multiline string, which is obviously the input from the website. This field is in the same local scope as the code for each day, so that when the IIFE for a day gets executed, the input is ready to be used.

The week

As you will see the code evolved a bit and I didn't stick 100% to the rules, especially on day 6. And with the evolution of the code, the rules also changed and evolved and so I would say without going back and "prettifying" the previous days, not every day is a "perfect" solution.

Day 1: Historian Hysteria

1234567891011121314151617181920212223
(
(
solveA: (arr0: number[], arr1: number[]) => number,
solveB: (arr0: number[], arr1: number[]) => number,
input: number[][]
) =>
((arr0, arr1) =>
console.log(`Part A: ${solveA(arr0, arr1)}\nPart B: ${solveB(arr0, arr1)}`))(
input.map((i) => i[0]).sort((a, b) => a - b),
input.map((i) => i[1]).sort((a, b) => a - b)
)
)(
(arr0, arr1) =>
arr0.reduce((prev, curr, i) => prev + Math.abs(curr - arr1[i]), 0),
(arr0, arr1) =>
arr0
.map((v0) => v0 * arr1.filter((v1) => v1 === v0).length)
.reduce((prev, curr) => prev + curr),
input
.trim()
.split("\n")
.map((l) => l.split(" ").map((v) => +v))
);

Here you can see the whole shebang in all it's glory. As you can see, I am actually using TypeScript, which should prevent me from doing stupid mistakes. But it doesn't prevent me from writing stupid code. The first day was really quite easy, so let's continue with day 2.

Day 2: Red-Nosed Reports

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
((solveA, solveB, input: number[][]) =>
console.log(`Part A: ${solveA(input)}\nPart B: ${solveB(input)}`))(
(input: number[][]) =>
input.filter(
(rep) =>
rep
.slice(1)
.map((v, i) => rep[i] - v)
.reduce(
(prev, curr) => [
prev[0] &&
Math.abs(curr) >= 1 &&
Math.abs(curr) <= 3 &&
(prev[1] === 0 || prev[1] === Math.sign(curr)),
prev[1] === 0 ? Math.sign(curr) : prev[1],
],
[true, 0]
)[0]
).length,
(input: number[][]) =>
input.filter(
(rep) =>
rep
.slice(1)
.map((v, i) => rep[i] - v)
.reduce(
(prev, curr) => {
if (!prev[0]) {
return prev;
}
const hasSign = prev[1] !== 0;
const nextSign = !hasSign ? Math.sign(curr) : prev[1];
let valid =
Math.abs(curr) >= 1 &&
Math.abs(curr) <= 3 &&
(!hasSign || prev[1] === Math.sign(curr));
let damped = prev[2];
if (!valid && !damped) {
damped = true;
valid = true;
}
return [valid, nextSign, damped];
},
[true, 0, false]
)[0]
).length,
input
.trim()
.split("\n")
.map((l) => l.split(" ").map((v) => +v))
);

Here you can see one of the days where I didn't fully commit to the rules. But still being the first day, these rules didn't exist yet. I actually went back to make it less readable!

This was also just getting me started, so nothing special here.

Day 3: Mull It Over

12345678910111213141516171819
((solve, inputA: string[] | null, inputB: string[] | null) =>
console.log(`Part A: ${solve(inputA)}\nPart B: ${solve(inputB)}`))(
(input: string[] | null) =>
input
?.map((mul) =>
mul
.slice(4, -1)
.split(",")
.map((v) => +v)
)
.map((mul) => mul[0] * mul[1])
.reduce((prev, curr) => prev + curr, 0),
input.match(/mul(d{1,3},d{1,3})/g),
input
.split("do()")
.map((dos) => dos.split("don't()")[0])
.join("")
.match(/mul(d{1,3},d{1,3})/g)
);

This is quite the short one. As you can see I used a fancy regex to find all valid instructions and then parsed them out. I think I also solved the second part quite elegantly, splitting the input beforehand on dos, and splitting the dos on donts, discarding everything after the first entry. This leaves only the sections between a do and a dont to check with the regex.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
((
transpose: (matrix: string[][]) => string[][],
diagonals: (matrix: string[][]) => string[][],
flipX: (matrix: string[][]) => string[][]
) =>
((solveA, solveB, input) =>
console.log(`Part A: ${solveA(input)}\nPart B: ${solveB(input)}`))(
(matrix: string[][]) =>
[
...matrix,
...transpose(matrix),
...diagonals(matrix),
...diagonals(flipX(matrix)),
]
.map((line) => line.join(""))
.map(
(line) =>
(line.match(/XMAS/g)?.length ?? 0) +
(line.match(/SAMX/g)?.length ?? 0)
)
.reduce((p, c) => p + c, 0),
(matrix: string[][]) =>
[
matrix,
transpose(matrix),
flipX(matrix),
flipX(transpose(matrix)),
].reduce(
(prev, mat) =>
mat.reduce(
(prev, row, y) =>
row.reduce(
(prev, _, x) =>
+[/M.S/, /.A./, /M.S/].every((reg, i) =>
reg.test(mat[y + i]?.slice(x, x + 3).join(""))
) + prev,
0
) + prev,
0
) + prev,
0
),
input
.trim()
.split("\n")
.map((line) => line.split(""))
))(
(matrix) =>
matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex])),
(matrix) =>
Array(matrix.length + matrix[0].length)
.fill(0)
.map((_, dIdx) =>
matrix.reduce(
(arr, row, y) => [...row.filter((_, x) => x + y === dIdx), ...arr],
[]
)
),
(matrix) => matrix.map((line) => line.reduce((p, c) => [c, ...p], [] as string[]))
);

This one was a doozy. As you might (or might not) see, I am transforming the whole board to check rows with a regex. Also instead of just finding all As for the second part and checking its corners, I bruteforce myself through the whole board. Nobody ever said this should be efficient, right?

With this one, I even pass the helper functions to the IIFE to keep it in one line.

Day 5: Print Queue

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
((solveA, solveB, input: [[number, number][], number[][]]) =>
console.log(`Part A: ${solveA(input)}\nPart B: ${solveB(input)}`))(
([rules, updates]: [[number, number][], number[][]]) =>
updates
.filter(
(update) =>
![
rules.filter((rule) => rule.every((num) => update.includes(num))),
].some(
(rules) =>
!update.every((num, i) =>
rules
.filter((rule) => rule.includes(num))
.every(
(rule) =>
(rule[0] === num && update.indexOf(rule[1]) > i) ||
(rule[1] === num && update.indexOf(rule[0]) < i)
)
)
)
)
.map((arr) => arr[Math.floor(arr.length / 2)])
.reduce((p, c) => p + c, 0),
([rules, updates]: [[number, number][], number[][]]) =>
updates
.filter(
(update) =>
![
rules.filter((rule) => rule.every((num) => update.includes(num))),
].some(
(rules) =>
!update.some((num, i) =>
rules
.filter((rule) => rule.includes(num))
.some(
(rule) =>
(rule[0] === num && update.indexOf(rule[1]) < i) ||
(rule[1] === num && update.indexOf(rule[0]) > i)
)
)
)
)
.map((update) =>
update.sort((a, b) =>
((rule) => (rule ? rule.indexOf(a) - rule.indexOf(b) : 0))(
rules.find((rule) => rule.includes(a) && rule.includes(b))
)
)
)
.map((arr) => arr[Math.floor(arr.length / 2)])
.reduce((p, c) => p + c, 0),
input
.split("\n\n")
.map((val) => val.split("\n"))
.map((val, i) =>
i === 0
? val.map((rule) => rule.split("|"))
: val.map((update) => update.split(","))
)
.map((section) =>
section.map((line) => line.map((num) => +num))
) as [[number, number][], number[][]]
);

Here, something funny happened. I didn't read the instructions right and fumbled around for at least half an hour, wondering why it didn't work. Well, I should learn to read I guess.

The actual solution is once again quite easy and I can make use of JavaScripts array sorting method.

Day 6: Guard Gallivant

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
type Vec = {
x: number;
y: number;
};
type Field = {
blocked: boolean;
guard: string;
visited: string;
};
type Guard = {
pos: Vec;
dir: number;
};
const directions = [
{
x: 0,
y: -1,
},
{
x: 1,
y: 0,
},
{
x: 0,
y: 1,
},
{
x: -1,
y: 0,
},
];
function addVec(a: Vec, b: Vec): Vec {
return {
x: a.x + b.x,
y: a.y + b.y,
};
}
function retrieve<T>(arr: T[][], pos: Vec): T | undefined {
const line = arr[pos.y];
if (!line) {
return undefined;
}
return line[pos.x];
}
function insideBoard<T>(arr: T[][], pos: Vec): boolean {
return (
0 <= pos.y && pos.y < arr.length && 0 <= pos.x && pos.x < arr[0].length
);
}
function getBoard(text: string): Field[][] {
return text.split("\n").map((line) =>
line.split("").map((c) => ({
blocked: c == "#",
visited: "^>v<".includes(c) ? c : "",
guard: "^>v<".includes(c) ? c : "",
}))
);
}
function getGuard(arr: Field[][]): Guard {
return arr.reduce(
(p, line, y) => [
...p,
...line.reduce(
(p, c, x) =>
c.guard ? [...p, { pos: { x, y }, dir: "^>v<".indexOf(c.guard) }] : p,
[] as { pos: Vec; dir: number }[]
),
],
[] as { pos: Vec; dir: number }[]
)[0];
}
let board = getBoard(input);
let guard = getGuard(board);
do {
const nextPosition = addVec(guard.pos, directions[guard.dir]);
const nextDirection = (guard.dir + 1) % directions.length;
if (retrieve(board, nextPosition)?.blocked) {
guard.dir = nextDirection;
} else {
guard.pos = nextPosition;
const nextField = retrieve(board, nextPosition);
if (nextField && !nextField.visited.includes("^>v<"[guard.dir])) {
nextField.visited += "^>v<"[guard.dir];
}
}
} while (insideBoard(board, guard.pos));
const visitedFields = board.reduce(
(p, line, y) => [
...p,
...line.reduce(
(p, val, x) => (val.visited.length > 0 ? [...p, { x, y }] : p),
[] as Vec[]
),
],
[] as Vec[]
);
console.log(visitedFields.length);
const obstacles: Vec[] = [];
for (const pos of visitedFields) {
board = getBoard(input);
guard = getGuard(board);
board[pos.y][pos.x].blocked = true;
let notlooped = true;
do {
const nextPosition = addVec(guard.pos, directions[guard.dir]);
const nextDirection = (guard.dir + 1) % directions.length;
if (retrieve(board, nextPosition)?.blocked) {
guard.dir = nextDirection;
} else {
guard.pos = nextPosition;
const nextField = retrieve(board, nextPosition);
if (nextField) {
if (!nextField.visited.includes("^>v<"[guard.dir])) {
nextField.visited += "^>v<"[guard.dir];
} else {
notlooped = false;
}
}
}
} while (insideBoard(board, guard.pos) && notlooped);
if (!notlooped) {
obstacles.push(pos);
}
}
console.log(obstacles.length);

On this day, I definitely broke all of my own rules. I don't think the effort it would take is worth it, you can really see how this is by far the longest one. Of course there are a few type declarations at the beginning, but still.

While the first part was really trivial, the second part took me longer than I imagined. I ended up checking all cells the guard visits and if blocking it would result in a loop (Meaning he reaches a cell he already visited, going in the same direction). I guess that is a smart way to brute force things.

Day 7: Bridge Repair

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
((
solve: (
input: {
result: number;
numbers: number[];
}[],
operators: string[]
) => number,
input: {
result: number;
numbers: number[];
}[]
) =>
console.log(
`Part A: ${solve(input, ["+", "*"])}\nPart B: ${solve(input, ["+", "*", "||"])}`
))(
(input, operators) =>
input.reduce(
(p, { result, numbers }) =>
Array(Math.pow(operators.length, numbers.length - 1))
.fill(0)
.some(
(_, i) =>
result ===
((ops) =>
numbers
.slice(1)
.map((n, i) => ({ n, o: ops[i] }))
.reduce(
(p, { n, o }) =>
o === "+" ? p + n : o === "*" ? p * n : +`${p}${n}`,
numbers[0]
))(
Number(i)
.toString(operators.length)
.padStart(numbers.length - 1, "0")
.split("")
.map((v) => operators[+v])
)
)
? p + result
: p,
0
),
input.split("\n").map((line) =>
(([res, numbers]) => ({
result: +res,
numbers: numbers?.split(" ").map((num) => +num),
}))(line.split(": "))
)
);

I gotta say, today was definitely the most fun I had so far. The whole thing is really just about binary counting. The code in line 34-38 is really just converting number into base 2 or 3 and then mapping this number onto the operators, resulting in a set of operators to use for each equation.

I actually solved this using a version where I created an array with all possible combinations of operators but this definitely saves a bit of memory.

And why am I basically brute forcing stuff, again?

Closing words (for this week)

While I either had in mind to not take part or to do it fancy, I did not expect to do it this sloppily.

But taking part in that way is better than not taking part. Also this week had a weird focus on brute forcing, or at least brute forcing in a smart way. I think I found these puzzles way easier than the one from the last years, maybe my perspective is just skewed.

Anyway, hopefully till next week!