From 852515af6f65c0dc733a9f7525df06ae1189c83f Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Tue, 21 Jan 2025 21:47:24 -0800 Subject: [PATCH] (Small!) partial solution to 20-1 --- inputs/20/test.txt | 15 ++++++ solutions/20.zig | 113 +++++++++++++++++++++++++++++++++++++++++++++ solutions/util.zig | 97 ++++++++++++++++++-------------------- 3 files changed, 172 insertions(+), 53 deletions(-) create mode 100644 inputs/20/test.txt create mode 100644 solutions/20.zig diff --git a/inputs/20/test.txt b/inputs/20/test.txt new file mode 100644 index 0000000..f107d40 --- /dev/null +++ b/inputs/20/test.txt @@ -0,0 +1,15 @@ +############### +#...#...#.....# +#.#.#.#.#.###.# +#S#...#.#.#...# +#######.#.#.### +#######.#.#...# +#######.#.###.# +###..E#...#...# +###.#######.### +#...###...#...# +#.#####.#.###.# +#.#...#.#.#...# +#.#.#.#.#.#.### +#...#...#...### +############### \ No newline at end of file diff --git a/solutions/20.zig b/solutions/20.zig new file mode 100644 index 0000000..bb469b9 --- /dev/null +++ b/solutions/20.zig @@ -0,0 +1,113 @@ +const std = @import("std"); +const print = std.debug.print; +const util = @import("util.zig"); +const Point = util.Point; +const log = util.log; +const expect = std.testing.expect; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const response = try partOne(false, false, allocator); + print("{}\n", .{response}); +} + +// Sketch of intended logic: +// * Find shortest non-cheating time start-to-finish +// * Find fastest time (don't need to save path) from start _to_ each location +// * Find faster time (ditto) from end _to_ each location +// * Find all cheats +// * For each cheat: +// * Time saved is basic time - (time-from-start-to-start-of-cheat - time-from-end-of-cheat-to-end) +// +// Implementation is not yet complete! So far I've only implemented the first bullet, because building a generic +// implementation of Dijkstra's was an _ARSE_ - the rest can happen tomorrow! +fn partOne(is_test_case: bool, debug: bool, allocator: std.mem.Allocator) !u32 { + const input_file = try util.getInputFile("20", is_test_case); + const data = try util.readAllInputWithAllocator(input_file, allocator); + defer allocator.free(data); + + const map = buildMap(data, allocator); + defer allocator.free(map); + defer { + for (map) |line| { + allocator.free(line); + } + } + + // Technically slightly inefficient to do it this way, as we could have done it during `buildMap`, but I prefer my + // functions to do one-and-only-one thing. + const start_point = findPoint(map, 'S'); + const end_point = findPoint(map, 'E'); + log("Start point is {s} and end point is {s}\n", .{ start_point, end_point }, debug); + + const neighboursFunc = &struct { + pub fn func(d: *const [][]u8, point: *Point, alloc: std.mem.Allocator) []Point { + var response = std.ArrayList(Point).init(alloc); + const ns = point.neighbours(d.*[0].len, d.len, alloc); + for (ns) |n| { + if (d.*[n.y][n.x] != '#') { + response.append(n) catch unreachable; + } + } + alloc.free(ns); + return response.toOwnedSlice() catch unreachable; + } + }.func; + + const shortestPathLength = util.dijkstra([][]u8, Point, &map, neighboursFunc, start_point, end_point, debug, allocator) catch unreachable; + return shortestPathLength; +} + +fn buildNeighboursFunction(map: *const [][]u8) *const fn (p: *Point, alloc: std.mem.Allocator) []Point { + return struct { + pub fn call(p: *Point, alloc: std.mem.Allocator) []Point { + var responseList = std.ArrayList(Point).init(alloc); + const neighbours = p.neighbours(map[0].len, map.len, alloc); + for (neighbours) |n| { + if (map[n.y][n.x] == '.') { + responseList.append(n) catch unreachable; + } + } + alloc.free(neighbours); + + return responseList.toOwnedSlice(); + } + }.call; +} + +fn buildMap(data: []const u8, allocator: std.mem.Allocator) [][]u8 { + var map_list = std.ArrayList([]u8).init(allocator); + var data_iterator = std.mem.splitScalar(u8, data, '\n'); + while (data_iterator.next()) |data_line| { + var line = std.ArrayList(u8).init(allocator); + for (data_line) |c| { + line.append(c) catch unreachable; + } + map_list.append(line.toOwnedSlice() catch unreachable) catch unreachable; + } + return map_list.toOwnedSlice() catch unreachable; +} + +fn findPoint(data: [][]u8, char: u8) Point { + for (data, 0..) |line, y| { + for (line, 0..) |c, x| { + if (c == char) { + return Point{ .x = x, .y = y }; + } + } + } + unreachable; +} + +test "partOne" { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const response = try partOne(true, true, allocator); + print("{}\n", .{response}); + try expect(response == 84); +} diff --git a/solutions/util.zig b/solutions/util.zig index 289ccbe..a4526ee 100644 --- a/solutions/util.zig +++ b/solutions/util.zig @@ -138,26 +138,33 @@ pub fn log(comptime message: []const u8, args: anytype, debug: bool) void { // Assumes that all links have cost 1. const DijkstraError = error{NoPathFound}; -// I hate that I have to pass in an allocator to `neighbours`, but it seems necessary in order to be able to free -// whatever it returns. -pub fn dijkstra(T: type, neighbours: *const fn (t: *T, allocator: std.mem.Allocator) []T, start: T, end: T, debug: bool, allocator: std.mem.Allocator) DijkstraError!u32 { - var visited = std.AutoHashMap(T, void).init(allocator); +// Zig does not really support the passing-in of bare anonymous functions that depend on higher-level variables - you'll +// get errors like `'' not accessible from inner function` or `crossing function boundary`. +// +// This appears to be a deliberate design decision to avoid unintentional use-after-free: +// https://ziggit.dev/t/closure-in-zig/5449 +// +// So, instead of passing in a nicely-encapsulated partial-application `getNeighbours` which _just_ takes a `node_type`, it needs to take in the data as well. Blech. +// +// (Check out the implementation at commit `d85d29` to see what it looked like before this change!) +pub fn dijkstra(comptime data_type: type, comptime node_type: type, data: *const data_type, getNeighbours: *const fn (d: *const data_type, n: *node_type, allocator: std.mem.Allocator) []node_type, start: node_type, end: node_type, debug: bool, allocator: std.mem.Allocator) DijkstraError!u32 { + var visited = std.AutoHashMap(node_type, void).init(allocator); defer visited.deinit(); - var distances = std.AutoHashMap(T, u32).init(allocator); + var distances = std.AutoHashMap(node_type, u32).init(allocator); defer distances.deinit(); distances.put(start, 0) catch unreachable; // Not strictly necessary - we could just iterate over all keys of `distances` and filter out those that are // `visited` - but this certainly trims down the unnecessary debug logging, and I have an intuition (though haven't // proved) that it'll slightly help performance. - var unvisited_candidates = std.AutoHashMap(T, void).init(allocator); + var unvisited_candidates = std.AutoHashMap(node_type, void).init(allocator); defer unvisited_candidates.deinit(); unvisited_candidates.put(start, {}) catch unreachable; return while (true) { var cand_it = unvisited_candidates.keyIterator(); - var curr: T = undefined; + var curr: node_type = undefined; var lowest_distance_found: u32 = std.math.maxInt(u32); while (cand_it.next()) |cand| { const actual_candidate = cand.*; // Necessary to avoid pointer weirdness @@ -188,7 +195,7 @@ pub fn dijkstra(T: type, neighbours: *const fn (t: *T, allocator: std.mem.Alloca // Haven't terminated yet => we're still looking. Check neighbours, and update their min-distance const distance_of_neighbour_from_current = lowest_distance_found + 1; - const neighbours_of_curr = neighbours(&curr, allocator); + const neighbours_of_curr = getNeighbours(data, &curr, allocator); for (neighbours_of_curr) |neighbour| { if (visited.contains(neighbour)) { continue; @@ -213,35 +220,13 @@ pub fn dijkstra(T: type, neighbours: *const fn (t: *T, allocator: std.mem.Alloca }; } -// I tried declaring this within the `test "Dijkstra`", but this method of anonymous functions: -// https://gencmurat.com/en/posts/zig-anonymus-functions-and-closures/ -// didn't work for me when trying to pass an Allocator down inside. The following attempt: -// -// ``` -// const curry = struct { -// pub fn call(T: type, alloc: std.mem.Allocator) *const fn (t: T) []T { -// const Context = struct { alloc: std.mem.Allocator }; -// -// const context = Context{ .alloc = alloc }; -// -// return struct { -// pub fn call(p: Point) []Point { -// const response = std.ArrayList(Point).init(context.alloc); -// for (p.neighbours(15, 15, context.alloc)) |n| { -// if (data[16 * n.y + n.x] == '.') { -// response.append(n) catch unreachable; -// } -// } -// return response.toOwnedSlice() catch unreachable; -// } -// }.call; -// } -// }.call; -// ``` -// gave `'context' not accessible from inner function` -fn private_dijkstra_test_neighbours_function(p: *Point, allocator: std.mem.Allocator) []Point { +test "Dijkstra" { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + // From AoC 2024 Day 20 - const data = + const base_data = \\############### \\#...#...#.....# \\#.#.#.#.#.###.# @@ -258,27 +243,33 @@ fn private_dijkstra_test_neighbours_function(p: *Point, allocator: std.mem.Alloc \\#...#...#...### \\############### ; - - var response = std.ArrayList(Point).init(allocator); - const ns = p.neighbours(15, 15, allocator); - for (ns) |n| { - if (data[16 * n.y + n.x] == '.') { - response.append(n) catch unreachable; - } + // This is absolutely fucking ridiculous - but I can't find a way to create a `*const []u8` from the above + // `*const [N:0]u8`. + // In particular, `std.mem.span` doesn't work, contra https://stackoverflow.com/a/72975237 + var data_list = std.ArrayList(u8).init(allocator); + for (base_data) |c| { + data_list.append(c) catch unreachable; } - allocator.free(ns); - return response.toOwnedSlice() catch unreachable; -} - -test "Dijkstra" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - + const data = data_list.toOwnedSlice() catch unreachable; + defer allocator.free(data); const start = Point{ .x = 1, .y = 3 }; const end = Point{ .x = 5, .y = 7 }; - const result = dijkstra(Point, private_dijkstra_test_neighbours_function, start, end, false, allocator) catch unreachable; + const neighboursFunc = &struct { + pub fn func(d: *const []u8, point: *Point, alloc: std.mem.Allocator) []Point { + var response = std.ArrayList(Point).init(alloc); + const ns = point.neighbours(15, 15, alloc); + for (ns) |n| { + if (d.*[16 * n.y + n.x] == '.') { + response.append(n) catch unreachable; + } + } + alloc.free(ns); + return response.toOwnedSlice() catch unreachable; + } + }.func; + + const result = dijkstra([]u8, Point, &data, neighboursFunc, start, end, false, allocator) catch unreachable; // print("Dijkstra result is {}\n", .{result}); try expect(result == 84); }