Solution for 08-02

This commit is contained in:
Jack Jackson 2024-12-27 22:25:21 -08:00
parent 43982a67a1
commit c2955cd03e
3 changed files with 166 additions and 15 deletions

View File

@ -204,4 +204,4 @@ fn accumulate() ![]u32 {
## What's the point in `HashMap.getOrPut`?
`getOrPut` _doesn't_ actually `put` anything, it _only_ `get`s. See https://ziggit.dev/t/whats-the-point-in-hashmap-getorput/7547.
`getOrPut` _doesn't_ actually `put` anything, it _only_ `get`s. See https://ziggit.dev/t/whats-the-point-in-hashmap-getorput/7547.

View File

@ -10,4 +10,8 @@ I've tried (in `main.zig`) to make a general-purpose executable that can be pass
So for now, run directly with (e.g.) `zig run solutions/01.zig`, and do the following manual changes:
* Change `pub fn main() void {...}` in each solution-file to invoke the function you want run.
* Change `isTestCase` from `true` to `false` when ready to get the real solution.
* Change `isTestCase` from `true` to `false` when ready to get the real solution.
# Code Quality
AoC challenges almost always have a "twist" partway through, meaning that you can solve the second part by injecting one subtly-different piece of logic into the solution to the first part - a different way of calculating a value or identifying candidates. If I were trying to show off for an interview (and were more comfortable with the language!), I would do the refactoring "right" by factoring out the common setup and execution logic to sub-functions, so that `part_one` and `part_two` are each single-line invocations of a common `execute` function with differing functions passed as parameter. But this is just an exercise for myself to learn the language - I'd rather get to grips with challenging problems to learn techniques, than to learn the (language-agnostic) skills of refactoring that I am already _reasonably_ proficient with.

View File

@ -3,13 +3,17 @@ const print = std.debug.print;
const util = @import("util.zig");
pub fn main() !void {
const response = try part_one(false);
const response = try part_two(false);
print("{}\n", .{response});
}
const Point = struct { x: usize, y: usize };
const Point = struct { x: u16, y: u16 };
fn part_one(is_test_case: bool) !usize {
return execute(is_test_case, findAntinodes);
}
fn execute(is_test_case: bool, antinode_determination_function: fn (nodes: [2]Point, width: usize, height: usize, allocator: std.mem.Allocator) anyerror![]Point) !usize {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
@ -17,19 +21,19 @@ fn part_one(is_test_case: bool) !usize {
const input_file = try util.getInputFile("08", is_test_case);
const data = try util.readAllInputWithAllocator(input_file, allocator);
defer allocator.free(data);
print("DEBUG - created data", .{});
print("DEBUG - created data\n", .{});
// In this problem I'm experimenting with not even parsing the input into lines, but just keeping a "line counter"
// that is incremented whenever we hit a `\n` character
var width: ?usize = null;
var height: ?usize = null;
var x: usize = 0;
var y: usize = 0;
var x: u16 = 0;
var y: u16 = 0;
var antennae = std.AutoHashMap(u8, std.ArrayList(Point)).init(allocator);
defer antennae.deinit();
for (data) |c| {
print("DEBUG - checking {c} at {}, {}\n", .{ c, x, y });
// print("DEBUG - checking {c} at {}, {}\n", .{ c, x, y });
switch (c) {
'\n' => {
if (width == null) {
@ -47,6 +51,7 @@ fn part_one(is_test_case: bool) !usize {
var result = try antennae.getOrPut(c);
if (!result.found_existing) {
result.value_ptr.* = std.ArrayList(Point).init(allocator);
defer result.value_ptr.deinit();
}
try result.value_ptr.append(point);
x += 1;
@ -65,7 +70,7 @@ fn part_one(is_test_case: bool) !usize {
const node_pairs = try pairs(v.items, allocator);
defer allocator.free(node_pairs);
for (node_pairs) |node_pair| {
const antinodes = try findAntinodes(node_pair, width.?, height.?, allocator);
const antinodes = try antinode_determination_function(node_pair, width.?, height.?, allocator);
defer allocator.free(antinodes);
for (antinodes) |antinode| {
try all_antinodes.put(antinode, 1); // We don't actually need to put any value - just populating the key
@ -81,6 +86,31 @@ fn part_one(is_test_case: bool) !usize {
print("{}\n", .{antinode});
count += 1;
}
// SERIOUS debugging here - visualizing the output!
// var lines = std.ArrayList([]u8).init(allocator);
// defer lines.deinit();
// for (0..height.?) |_| {
// var line = std.ArrayList(u8).init(allocator);
// defer line.deinit();
// for (0..width.?) |_| {
// try line.append('.');
// }
// try lines.append(line.items);
// }
// var lines_items = lines.items;
// var antinode_iterator_for_visualization = all_antinodes.keyIterator();
// while (antinode_iterator_for_visualization.next()) |antinode| {
// lines_items[antinode.y][antinode.x] = '#';
// }
// print("DEBUG - grid is\n", .{});
// for (lines.items) |line| {
// for (line) |c| {
// print("{c}", .{c});
// }
// print("\n", .{});
// }
// End of debugging visualization
return count;
}
@ -97,7 +127,7 @@ fn pairs(nodes: []Point, allocator: std.mem.Allocator) ![][2]Point {
return output.toOwnedSlice();
}
fn findAntinodes(nodes: [2]Point, width: usize, height: usize, allocator: std.mem.Allocator) ![]Point {
fn findAntinodes(nodes: [2]Point, width: u16, height: u16, allocator: std.mem.Allocator) ![]Point {
var response = std.ArrayList(Point).init(allocator);
defer response.deinit();
@ -118,17 +148,134 @@ fn findAntinodes(nodes: [2]Point, width: usize, height: usize, allocator: std.me
return response.toOwnedSlice();
}
fn antiNodeIsValid(antiNode: Point, width: usize, height: usize) bool {
fn antiNodeIsValid(antiNode: Point, width: u16, height: u16) bool {
// Don't technically need to check for >= 0 because that's already checked in `findAntinodes` (because otherwise
// there would be integer overflow by daring to use a negative number :P ), but doesn't hurt to replicate it here -
// otherwise a future reader might think we've forgotten it.
return antiNode.x >= 0 and antiNode.y >= 0 and antiNode.x < width and antiNode.y < height;
}
fn part_two(is_test_case: bool) !usize {
return execute(is_test_case, findAntiNodesHarmonic);
}
fn findAntiNodesHarmonic(nodes: [2]Point, width: usize, height: usize, allocator: std.mem.Allocator) ![]Point {
print("DEBUG - checking antiNodesHarmonic for {} and {}\n", .{ nodes[0], nodes[1] });
// Approach:
// * Find the vector V from nodes[0] to nodes[1]
// * Find greatest-common-factor of V.x and V.y
// * Use that to find the smallest integer-step <V'.x, V'.y>
// * Iteratively (starting from n=0), check nodes[0] + n*V' for legality - then same for subtraction
//
// Not making a type for `Vector` because idk how to have a signed size, but it'd be a reasonable approach!
const vector_x: i32 = @as(i32, nodes[1].x) - @as(i32, nodes[0].x);
const vector_y: i32 = @as(i32, nodes[1].y) - @as(i32, nodes[0].y);
const greatest_common_factor = gcf(magnitude(vector_x), magnitude(vector_y));
const mini_vector_x = divide(vector_x, greatest_common_factor);
const mini_vector_y = divide(vector_y, greatest_common_factor);
print("DEBUG - vector_x is {}, mini_vector_x is {}, vector_y is {}, mini_vector_y is {}, gcd is {}\n", .{ vector_x, mini_vector_x, vector_y, mini_vector_y, greatest_common_factor });
var candidates = std.ArrayList(Point).init(allocator);
defer candidates.deinit();
var step: i32 = 0;
while (true) : (step += 1) {
var skips: usize = 0;
const x_step: i32 = step * mini_vector_x;
const y_step: i32 = step * mini_vector_y;
print("DEBUG - step is {}, x_step is {}, y_step is {}\n", .{ step, x_step, y_step });
if (nodes[0].x < x_step or nodes[0].y < y_step or (x_step < 0 and (magnitude(x_step) + nodes[0].x >= width)) or (y_step < 0 and (magnitude(y_step) + nodes[0].y >= height))) {
skips += 1;
} else {
const candidate_x: u16 = @intCast(nodes[0].x - x_step);
const candidate_y: u16 = @intCast(nodes[0].y - y_step);
try candidates.append(Point{ .x = candidate_x, .y = candidate_y });
}
if (nodes[0].x + x_step >= width or nodes[0].y + y_step >= height or (x_step < 0 and magnitude(x_step) > nodes[0].x) or (y_step < 0 and magnitude(y_step) > nodes[0].y)) {
skips += 1;
} else {
const candidate_x: u16 = @intCast(nodes[0].x + x_step);
const candidate_y: u16 = @intCast(nodes[0].y + y_step);
try candidates.append(Point{ .x = candidate_x, .y = candidate_y });
}
if (skips == 2) {
break;
}
}
const response = candidates.toOwnedSlice();
print("Response is {any}\n", .{response});
return response;
}
// There _must_ be something like this in the standard library, but I couldn't find it at a glance.
fn magnitude(num: i32) u32 {
if (num < 0) {
return @intCast(-num);
} else {
return @intCast(num);
}
}
fn gcf(larger: u32, smaller: u32) u32 {
print("DEBUG - calculating gcf for {} and {}\n", .{ larger, smaller });
var a = larger;
const b = smaller;
if (b > a) {
print("DEBUG - reversing them\n", .{});
return gcf(b, a);
}
while (a > b) {
print("DEBUG - subtracting {} from {} ", .{ b, a }); // Note no line-break!
a -= b;
print("to get {}\n", .{a});
}
if (a == b) {
print("DEBUG - a == b, returning the value ({})\n", .{a});
return a;
} else {
// a!>b and a!=b => a < b
print("DEBUG - b is now larger than a ({}, {}), so starting again\n", .{ b, a });
return gcf(b, a);
}
}
// Again - this _must_ exist somewhere in the standard library, can't believe I'm missing it
fn divide(num: i32, denom: u32) i32 {
const denom_as_i32: i32 = @intCast(denom);
const divided_value: i32 = @divExact(num, denom_as_i32);
return divided_value;
}
const expect = std.testing.expect;
test "part_one" {
const part_one_response = try part_one(true);
print("DEBUG - part_one_response is {}\n", .{part_one_response});
try expect(part_one_response == 14);
// test "part_one" {
// const part_one_response = try part_one(true);
// print("DEBUG - part_one_response is {}\n", .{part_one_response});
// try expect(part_one_response == 14);
// }
test "greatest_common_factor" {
try expect(gcf(18, 27) == 9);
try expect(gcf(182664, 154875) == 177);
}
test "magnitude" { // Pop pop!
try expect(magnitude(5) == 5);
try expect(magnitude(-32) == 32);
}
test "divide" {
try expect(divide(4, 2) == 2);
try expect(divide(-4, 2) == -2);
try expect(divide(25, 5) == 5);
try expect(divide(-50, 2) == -25);
}
test "part_two" {
const part_two_response = try part_two(true);
print("DEBUG - part_two_response is {}\n", .{part_two_response});
try expect(part_two_response == 34);
}