diff --git a/README.md b/README.md index 020cdbe..552976c 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,7 @@ So for now, run directly with (e.g.) `zig run solutions/01.zig`, and do the foll # 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. + +# Retrospective + +I aimed to complete as many problems as I could by the end of 2024, and as of writing this (at 17:02 on 2024-12-31, with a NYE party to get to), it looks unlikely that I'll get beyond Day 10. That's [one better than last year's](https://github.com/scubbo/advent-of-code-2023/tree/main/src) - and considering that I got married during this December, I think that's a pretty respectable showing 😅 I'd definitely like to go back and complete all the problems during January, though, as well as keep asking the helpful folks in [Ziggit.dev](https://ziggit.dev) more newbie-questions to help me actually _understand_ the language rather than simply shuffling ideas around until I get a non-erroring result. diff --git a/solutions/10.zig b/solutions/10.zig index e88078c..15fdd2a 100644 --- a/solutions/10.zig +++ b/solutions/10.zig @@ -3,7 +3,7 @@ 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}); } @@ -66,6 +66,64 @@ fn part_one(is_test_case: bool) anyerror!u64 { return total; } +fn getReachablePeaks(grid: [][]u32, start: Location, start_value: u32, so_far: *std.AutoHashMap(Location, bool), alloc: std.mem.Allocator) ?anyerror { + if (start_value == 9) { + try so_far.put(start, true); + return null; + } + + // Check up, down, left, right - if they have the right next value, iterate from there + const neighbours = try buildNeighbours(grid, start, alloc); + for (neighbours) |neighbour| { + if (grid[neighbour.y][neighbour.x] == start_value + 1) { + const err = getReachablePeaks(grid, neighbour, start_value + 1, so_far, alloc); + if (err != null) { + return err; + } + } + } + alloc.free(neighbours); + return null; +} + +// Funnily enough, I actually misread the question and implemented this logic _first_, and was confused why I kept +// getting test failures :P +fn part_two(is_test_case: bool) !u64 { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const input_file = try util.getInputFile("10", is_test_case); + const data = try util.readAllInputWithAllocator(input_file, allocator); + defer allocator.free(data); + + const grid = try buildGrid(data, allocator); + defer allocator.free(grid); + print("{any}\n", .{grid}); + + // If we wanted, we could find trailheads during the `buildGrid` iteration, but given the small data sizes I'd much + // rather keep small focused functions at the cost of some constant-factor performance. + var total: u32 = 0; + var i: usize = 0; + while (i < grid.len) : (i += 1) { + var j: usize = 0; + while (j < grid[0].len) : (j += 1) { + if (grid[i][j] == 0) { + const trailScore = try getTrailScore(grid, Location{ .x = j, .y = i }, 0, allocator); + total += trailScore; + print("DEBUG - for the trailhead at {}/{}, found a trailscore of {}\n", .{ j, i, trailScore }); + } + } + } + + // Wow I do _not_ like memory management + for (grid) |line| { + allocator.free(line); + } + + return total; +} + fn buildGrid(data: []const u8, alloc: std.mem.Allocator) ![][]u32 { var lines = std.ArrayList([]u32).init(alloc); defer lines.deinit(); @@ -111,24 +169,22 @@ fn buildNeighbours(grid: [][]u32, location: Location, alloc: std.mem.Allocator) return neighbours.toOwnedSlice(); } -fn getReachablePeaks(grid: [][]u32, start: Location, start_value: u32, so_far: *std.AutoHashMap(Location, bool), alloc: std.mem.Allocator) ?anyerror { +fn getTrailScore(grid: [][]u32, start: Location, start_value: u32, alloc: std.mem.Allocator) !u32 { if (start_value == 9) { - try so_far.put(start, true); - return null; + return 1; } // Check up, down, left, right - if they have the right next value, iterate from there + var total: u32 = 0; const neighbours = try buildNeighbours(grid, start, alloc); for (neighbours) |neighbour| { if (grid[neighbour.y][neighbour.x] == start_value + 1) { - const err = getReachablePeaks(grid, neighbour, start_value + 1, so_far, alloc); - if (err != null) { - return err; - } + total += try getTrailScore(grid, neighbour, start_value + 1, alloc); } } alloc.free(neighbours); - return null; + print("DEBUG - trail starting with value {}, at location {}/{}, has value {}\n", .{ start_value, start.y, start.x, total }); + return total; } const expect = std.testing.expect; @@ -138,3 +194,9 @@ test "part_one" { print("DEBUG - part_one_response is {}\n", .{part_one_response}); try expect(part_one_response == 36); } + +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 == 81); +}