(Small!) partial solution to 20-1
This commit is contained in:
parent
d85d294c58
commit
852515af6f
15
inputs/20/test.txt
Normal file
15
inputs/20/test.txt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
###############
|
||||||
|
#...#...#.....#
|
||||||
|
#.#.#.#.#.###.#
|
||||||
|
#S#...#.#.#...#
|
||||||
|
#######.#.#.###
|
||||||
|
#######.#.#...#
|
||||||
|
#######.#.###.#
|
||||||
|
###..E#...#...#
|
||||||
|
###.#######.###
|
||||||
|
#...###...#...#
|
||||||
|
#.#####.#.###.#
|
||||||
|
#.#...#.#.#...#
|
||||||
|
#.#.#.#.#.#.###
|
||||||
|
#...#...#...###
|
||||||
|
###############
|
113
solutions/20.zig
Normal file
113
solutions/20.zig
Normal file
@ -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);
|
||||||
|
}
|
@ -138,26 +138,33 @@ pub fn log(comptime message: []const u8, args: anytype, debug: bool) void {
|
|||||||
// Assumes that all links have cost 1.
|
// Assumes that all links have cost 1.
|
||||||
const DijkstraError = error{NoPathFound};
|
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
|
// Zig does not really support the passing-in of bare anonymous functions that depend on higher-level variables - you'll
|
||||||
// whatever it returns.
|
// get errors like `'<variable-name>' not accessible from inner function` or `crossing function boundary`.
|
||||||
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);
|
// 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();
|
defer visited.deinit();
|
||||||
|
|
||||||
var distances = std.AutoHashMap(T, u32).init(allocator);
|
var distances = std.AutoHashMap(node_type, u32).init(allocator);
|
||||||
defer distances.deinit();
|
defer distances.deinit();
|
||||||
distances.put(start, 0) catch unreachable;
|
distances.put(start, 0) catch unreachable;
|
||||||
|
|
||||||
// Not strictly necessary - we could just iterate over all keys of `distances` and filter out those that are
|
// 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
|
// `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.
|
// 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();
|
defer unvisited_candidates.deinit();
|
||||||
unvisited_candidates.put(start, {}) catch unreachable;
|
unvisited_candidates.put(start, {}) catch unreachable;
|
||||||
|
|
||||||
return while (true) {
|
return while (true) {
|
||||||
var cand_it = unvisited_candidates.keyIterator();
|
var cand_it = unvisited_candidates.keyIterator();
|
||||||
var curr: T = undefined;
|
var curr: node_type = undefined;
|
||||||
var lowest_distance_found: u32 = std.math.maxInt(u32);
|
var lowest_distance_found: u32 = std.math.maxInt(u32);
|
||||||
while (cand_it.next()) |cand| {
|
while (cand_it.next()) |cand| {
|
||||||
const actual_candidate = cand.*; // Necessary to avoid pointer weirdness
|
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
|
// 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 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| {
|
for (neighbours_of_curr) |neighbour| {
|
||||||
if (visited.contains(neighbour)) {
|
if (visited.contains(neighbour)) {
|
||||||
continue;
|
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:
|
test "Dijkstra" {
|
||||||
// https://gencmurat.com/en/posts/zig-anonymus-functions-and-closures/
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
// didn't work for me when trying to pass an Allocator down inside. The following attempt:
|
defer _ = gpa.deinit();
|
||||||
//
|
const allocator = gpa.allocator();
|
||||||
// ```
|
|
||||||
// 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 {
|
|
||||||
// From AoC 2024 Day 20
|
// 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
|
|||||||
\\#...#...#...###
|
\\#...#...#...###
|
||||||
\\###############
|
\\###############
|
||||||
;
|
;
|
||||||
|
// This is absolutely fucking ridiculous - but I can't find a way to create a `*const []u8` from the above
|
||||||
var response = std.ArrayList(Point).init(allocator);
|
// `*const [N:0]u8`.
|
||||||
const ns = p.neighbours(15, 15, allocator);
|
// In particular, `std.mem.span` doesn't work, contra https://stackoverflow.com/a/72975237
|
||||||
for (ns) |n| {
|
var data_list = std.ArrayList(u8).init(allocator);
|
||||||
if (data[16 * n.y + n.x] == '.') {
|
for (base_data) |c| {
|
||||||
response.append(n) catch unreachable;
|
data_list.append(c) catch unreachable;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
allocator.free(ns);
|
const data = data_list.toOwnedSlice() catch unreachable;
|
||||||
return response.toOwnedSlice() catch unreachable;
|
defer allocator.free(data);
|
||||||
}
|
|
||||||
|
|
||||||
test "Dijkstra" {
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
||||||
defer _ = gpa.deinit();
|
|
||||||
const allocator = gpa.allocator();
|
|
||||||
|
|
||||||
const start = Point{ .x = 1, .y = 3 };
|
const start = Point{ .x = 1, .y = 3 };
|
||||||
const end = Point{ .x = 5, .y = 7 };
|
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});
|
// print("Dijkstra result is {}\n", .{result});
|
||||||
try expect(result == 84);
|
try expect(result == 84);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user