From c2955cd03ebfa99d1b6fe2d5d871345f13c79a35 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Fri, 27 Dec 2024 22:25:21 -0800 Subject: [PATCH] Solution for 08-02 --- NOTES.md | 2 +- README.md | 6 +- solutions/08.zig | 173 +++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 166 insertions(+), 15 deletions(-) diff --git a/NOTES.md b/NOTES.md index 93f7969..b4dca7d 100644 --- a/NOTES.md +++ b/NOTES.md @@ -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. \ No newline at end of file +`getOrPut` _doesn't_ actually `put` anything, it _only_ `get`s. See https://ziggit.dev/t/whats-the-point-in-hashmap-getorput/7547. diff --git a/README.md b/README.md index 397580c..020cdbe 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file +* 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. diff --git a/solutions/08.zig b/solutions/08.zig index 164e183..a86d9fe 100644 --- a/solutions/08.zig +++ b/solutions/08.zig @@ -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 + // * 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); }