Day 8: Resonant Collinearity
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Kotlin
A bit late to the party, but hereβs my solution. I donβt know, if you even need to search for the smallest integer vector in the same direction in part 2, but I did it anyway.
Code:
import kotlin.math.abs
import kotlin.math.pow
fun main() {
fun part1(input: List<String>): Int {
val inputMap = Day08Map(input)
return inputMap.isoFrequencyNodeVectorsByLocations
.flatMap { (location, vectors) ->
vectors.map { (2.0 scaleVec it) + location }
}
.toSet()
.count { inputMap.isInGrid(it) }
}
fun part2(input: List<String>): Int {
val inputMap = Day08Map(input)
return buildSet {
inputMap.isoFrequencyNodeVectorsByLocations.forEach { (location, vectors) ->
vectors.forEach { vector ->
var i = 0.0
val scaledDownVector = smallestIntegerVectorInSameDirection2D(vector)
while (inputMap.isInGrid(location + (i scaleVec scaledDownVector))) {
add(location + (i scaleVec scaledDownVector))
i++
}
}
}
}.count()
}
val testInput = readInput("Day08_test")
check(part1(testInput) == 14)
check(part2(testInput) == 34)
val input = readInput("Day08")
part1(input).println()
part2(input).println()
}
tailrec fun gcdEuclid(a: Int, b: Int): Int =
if (b == 0) a
else if (a == 0) b
else if (a > b) gcdEuclid(a - b, b)
else gcdEuclid(a, b - a)
fun smallestIntegerVectorInSameDirection2D(vec: VecNReal): VecNReal {
assert(vec.dimension == 2) // Only works in two dimensions.
assert(vec == vec.roundComponents()) // Only works on integer vectors.
return (gcdEuclid(abs(vec[0].toInt()), abs(vec[1].toInt())).toDouble().pow(-1) scaleVec vec).roundComponents()
}
class Day08Map(input: List<String>): Grid2D<Char>(input.reversed().map { it.toList() }) {
init {
transpose()
}
val isoFrequencyNodesLocations = asIterable().toSet().filter { it != '.' }.map { frequency -> asIterable().indicesWhere { frequency == it } }
val isoFrequencyNodeVectorsByLocations = buildMap {
isoFrequencyNodesLocations.forEach { isoFrequencyLocationList ->
isoFrequencyLocationList.mapIndexed { index, nodeLocation ->
this[VecNReal(nodeLocation)] = isoFrequencyLocationList
.slice((0 until index) + ((index + 1)..isoFrequencyLocationList.lastIndex))
.map { VecNReal(it) - VecNReal(nodeLocation) }
}
}
}
}
C
Not hard but a little fiddly.
Code
#include "common.h"
#define GZ 52
static char g[GZ][GZ];
#define ANTI_P1 1
#define ANTI_P2 2
static uint8_t anti[GZ][GZ];
static int w,h;
int
main(int argc, char **argv)
{
int p1=0,p2=0, x,y, x1,y1, ax,ay, i;
char *lf;
if (argc > 1)
DISCARD(freopen(argv[1], "r", stdin));
for (h=0; h<GZ && fgets(g[h], GZ, stdin); h++)
;
assert(feof(stdin));
lf = strchr(g[0], '\n');
assert(lf);
w = lf - g[0];
/*
* Find antenna pairs, then project backwards from the first,
* forwards from the second. Don't like the repetition but it
* makes for easy code.
*/
for (y=0; y<h; y++)
for (x=0; x<w; x++) {
if (!isalnum(g[y][x]))
continue;
for (y1=y; y1<h; y1++)
for (x1=(y==y1?x+1:0); x1<w; x1++) {
if (g[y][x] != g[y1][x1])
continue;
for (i=0; ; i++) {
if ((ax = x-(x1-x)*i) <0 || ax>w ||
(ay = y-(y1-y)*i) <0 || ay>h)
break;
anti[ay][ax] |= ANTI_P1 * i==1;
anti[ay][ax] |= ANTI_P2;
}
for (i=0; ; i++) {
if ((ax = x1+(x1-x)*i) <0 || ax>w ||
(ay = y1+(y1-y)*i) <0 || ay>h)
break;
anti[ay][ax] |= ANTI_P1 * i==1;
anti[ay][ax] |= ANTI_P2;
}
}
}
for (y=0; y<h; y++)
for (x=0; x<w; x++) {
p1 += !!(anti[y][x] & ANTI_P1);
p2 += !!(anti[y][x] & ANTI_P2);
}
printf("08: %d %d\n", p1, p2);
return 0;
}
TypeScript
I was a little confuzzled with this one, but I managed to get it. :) Happy to know that I managed to reuse more of my code from previous days. I should write something to handle Vectors. It was sad to write my own basic, non-reusable thing.
Solution
import { AdventOfCodeSolutionFunction } from "./solutions";
// imported code:
export const check_coords = (grid: Array<Array<any>>, x: number, y: number) => {
return y >= grid.length ||
y < 0 ||
x >= grid[y].length ||
x < 0
}
export const makeGridFromMultilineString =
(input: string) => input.split("\n").map(st => st.trim()).map(v => v.split(""));
export const MakeEmptyGenericArray = <T>(length: number, fn: (index: number) => T) => {
const newArray = [];
let i = 0;
while (i++ < length)
newArray.push(fn(i));
return newArray;
}
export const MakeEmptyArray = (length: number) => MakeEmptyGenericArray(length, () => 0);
export const MakeEmpty2DArray = (x: number, y: number) => MakeEmptyArray(y).map(() => MakeEmptyArray(x));
// solution code
type v2 = [x: number, y: number];
const Sub = (x1: number, y1: number, x2: number, y2: number): v2 => {
return [x1 - x2, y1 - y2];
}
const Add = (x1: number, y1: number, x2: number, y2: number): v2 => {
return [x1 + x2, y1 + y2];
}
export const solution_8: AdventOfCodeSolutionFunction = (input) => {
const grid = makeGridFromMultilineString(input);
const nodes = new Map<string, Array<v2>>();
const nodeKinds: Array<string> = [];
const singleAntinodeLocations = MakeEmpty2DArray(grid.length, grid[0].length);
const resonantAntinodeLocations = MakeEmpty2DArray(grid.length, grid[0].length);
// find all the nodes
grid.forEach((row, y) => row.forEach((item, x) => {
if (item == ".")
return;
if (nodes.has(item))
nodes.get(item)!.push([x, y]);
else {
nodes.set(item, [[x, y]]);
nodeKinds.push(item);
}
}));
nodeKinds.forEach((nodeKind) => {
const nodesOfKind = nodes.get(nodeKind)!;
for (let bunn = 0; bunn < nodesOfKind.length; bunn++) {
const first = nodesOfKind[bunn];
for (let tort = bunn + 1; tort < nodesOfKind.length; tort++) {
// find antinode
const second = nodesOfKind[tort];
const diff = Sub(...first, ...second);
const [x1, y1] = Add(...first, ...diff);
const [x2, y2] = Sub(...second, ...diff);
if(!check_coords(singleAntinodeLocations, x1, y1)) singleAntinodeLocations[y1][x1]++;
if(!check_coords(singleAntinodeLocations, x2, y2)) singleAntinodeLocations[y2][x2]++;
// find all resonances
// starting
resonantAntinodeLocations[first[1]][first[0]]++;
resonantAntinodeLocations[second[1]][second[0]]++;
// go forward
let newFirst = [x1, y1] as v2;
while(!check_coords(resonantAntinodeLocations, ...newFirst)) {
let [x, y] = newFirst;
resonantAntinodeLocations[y][x]++;
newFirst = Add(...newFirst, ...diff);
}
// go back
newFirst = [x2, y2] as v2;
while(!check_coords(resonantAntinodeLocations, ...newFirst)) {
let [x, y] = newFirst;
resonantAntinodeLocations[y][x]++;
newFirst = Sub(...newFirst, ...diff);
}
}
}
});
const antinodeCount = (prev: number, curr: Array<number>) => prev + curr.reduce((prev, curr) => prev + (curr > 0 ? 1 : 0), 0);
const part_1 = singleAntinodeLocations.reduce<number>(antinodeCount, 0);
const part_2 = resonantAntinodeLocations.reduce<number>(antinodeCount, 0);
return {
part_1, //390
part_2, //1246
}
}
Loops on loops on loops on loopsβ¦
Rust
Proper Point and Vector types made this pretty simple, part 2 was just a tiny change (basically while
instead of if
), but left with a lot of copy-pasted code.
Solution
use euclid::default::*;
const N_ANTENNAS: usize = (b'z' - b'0') as usize + 1;
// For each frequency (from b'0' to b'z') the list of antenna positions
type Antennas = Box<[Vec<Point2D<i32>>]>;
fn parse(input: String) -> (Antennas, Rect<i32>) {
let mut antennas = vec![Vec::new(); N_ANTENNAS].into_boxed_slice();
let mut width = 0;
let mut height = 0;
for (y, l) in input.lines().enumerate() {
height = y + 1;
if width == 0 {
width = l.len()
} else {
assert!(width == l.len())
}
for (x, b) in l.bytes().enumerate().filter(|(_, b)| *b != b'.') {
antennas[(b - b'0') as usize].push(Point2D::new(x, y).to_i32())
}
}
let bounds = Rect::new(Point2D::origin(), Size2D::new(width, height).to_i32());
(antennas, bounds)
}
fn part1(input: String) {
let (antennas, bounds) = parse(input);
let mut antinodes = vec![vec![false; bounds.width() as usize]; bounds.height() as usize];
for list in antennas.iter().filter(|l| !l.is_empty()) {
for (i, &a) in list.iter().enumerate().skip(1) {
for &b in list.iter().take(i) {
let diff = b - a;
let ax = a - diff;
if bounds.contains(ax) {
antinodes[ax.y as usize][ax.x as usize] = true;
}
let bx = b + diff;
if bounds.contains(bx) {
antinodes[bx.y as usize][bx.x as usize] = true;
}
}
}
}
let sum = antinodes
.iter()
.map(|row| row.iter().map(|b| u32::from(*b)).sum::<u32>())
.sum::<u32>();
println!("{sum}");
}
fn part2(input: String) {
let (antennas, bounds) = parse(input);
let mut antinodes = vec![vec![false; bounds.width() as usize]; bounds.height() as usize];
for list in antennas.iter().filter(|l| !l.is_empty()) {
for (i, &a) in list.iter().enumerate().skip(1) {
for &b in list.iter().take(i) {
let diff = b - a;
// Start at antenna a, keep going until hitting bounds
let mut ax = a;
while bounds.contains(ax) {
antinodes[ax.y as usize][ax.x as usize] = true;
ax -= diff;
}
let mut bx = b;
while bounds.contains(bx) {
antinodes[bx.y as usize][bx.x as usize] = true;
bx += diff;
}
}
}
}
let sum = antinodes
.iter()
.map(|row| row.iter().map(|b| u32::from(*b)).sum::<u32>())
.sum::<u32>();
println!("{sum}");
}
util::aoc_main!();
also on github
I also did it in Rust, though mine is quite a bit more verbose than yours :). https://gitlab.com/bricka/advent-of-code-2024-rust/-/blob/main/src/days/day08.rs?ref_type=heads
Have you considered using a HashSet<(usize, usize)>
to store the visited locations instead of a 2d vector?
I try to use Vec
s instead of HashSet
s and maps whenever the key domain is reasonably small (just the grid in this case), simply because in the end direct memory access is a lot faster than always hashing values.
But looking at this case again, it is certainly a lot easier to have just antinodes.len()
at the end instead of counting all true values. This datastructure is also not really performance-critical, so a HashSet
is probably the cleaner choice here.
Uiua
Adapting the part one solution for part two took me longer than part one did today, but I didnβt want to change much anymore.
I even got scolded by the interpreter to split the evaluating line onto multiple ones because it got too long.
Canβt say itβs pretty but it does itβs job ^^β
Run with example input here
PartOne β (
&rs β &fo "input-8.txt"
β(β½Β¬β".\n".β΄)
βββ @\n.
:Β€β(:Β€-1β³)
β‘(β‘ββ)
β΄/βββ(β‘(-:β-Β°β)β§
β 2)
⧻β½Β¬:β(/+β+)ββ><,0
)
PartTwo β (
&rs β &fo "input-8.txt"
β(β½Β¬β".\n".β΄βΒ€
β½:ββ‘(>1⧻ββ)
)
βββ @\n.
:Β€β(:Β€-1β³)
β‘(β‘ββ)
βΈβ(
β§
β 2βΒ€
β‘(:Β€β-Β°β
β’(βββ-ββΈβ’
| β
(=0/++β><,0β’))
β‘βββ
)
)
β΄/ββ/ββ
⧻β½Β¬:β(/+β+)ββ><,0
)
&p "Day 8:"
&pf "Part 1: "
&p PartOne
&pf "Part 2: "
&p PartTwo