328 lines
14 KiB
Zig
328 lines
14 KiB
Zig
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 partTwo(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);
|
|
|
|
var 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');
|
|
// Cleanup the map so that cheats-to-the-end will still be legal
|
|
map[end_point.y][end_point.x] = '.';
|
|
map[start_point.y][start_point.x] = '.';
|
|
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;
|
|
|
|
var distances_map = util.dijkstra([][]u8, Point, &map, neighboursFunc, start_point, null, debug, allocator);
|
|
defer distances_map.deinit();
|
|
|
|
var distances_map_from_end = util.dijkstra([][]u8, Point, &map, neighboursFunc, end_point, null, debug, allocator);
|
|
defer distances_map_from_end.deinit();
|
|
|
|
const shortest_non_cheating_path = distances_map.get(end_point).?;
|
|
const cheats = findAllPossibleCheats(map, allocator);
|
|
defer allocator.free(cheats);
|
|
|
|
var scored_cheats = scoreCheats(cheats, shortest_non_cheating_path, distances_map, distances_map_from_end, debug, allocator);
|
|
defer scored_cheats.deinit();
|
|
|
|
var total: u32 = 0;
|
|
var it = scored_cheats.iterator();
|
|
const target_speedup: u8 = if (is_test_case) 12 else 100;
|
|
log("Here are the scored cheats:\n", .{}, debug);
|
|
while (it.next()) |e| {
|
|
if (e.value_ptr.* >= target_speedup) {
|
|
total += 1;
|
|
}
|
|
if (e.value_ptr.* > 0) {
|
|
log("{}: {}\n", .{ e.key_ptr.*, e.value_ptr.* }, debug);
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
const Cheat = struct {
|
|
start: Point,
|
|
end: Point,
|
|
pub fn format(self: Cheat, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
|
_ = fmt;
|
|
_ = options;
|
|
|
|
try writer.print("[{s},{s}]", .{ self.start, self.end });
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
fn findAllPossibleCheats(data: [][]u8, allocator: std.mem.Allocator) []Cheat {
|
|
var set = std.AutoHashMap(Cheat, void).init(allocator);
|
|
defer set.deinit();
|
|
|
|
for (data, 0..) |line, y| {
|
|
for (line, 0..) |c, x| {
|
|
var shouldPrintDebugStatements = false;
|
|
if (y == 7 and x == 7) {
|
|
shouldPrintDebugStatements = true;
|
|
}
|
|
log("In 7/7 case\n", .{}, shouldPrintDebugStatements);
|
|
if (c != '.') {
|
|
continue;
|
|
}
|
|
|
|
if (x + 2 < data[0].len and data[y][x + 2] == '.') {
|
|
set.put(Cheat{ .start = Point{ .x = x, .y = y }, .end = Point{ .x = x + 2, .y = y } }, {}) catch unreachable;
|
|
}
|
|
if (x >= 2 and data[y][x - 2] == '.') {
|
|
set.put(Cheat{ .start = Point{ .x = x, .y = y }, .end = Point{ .x = x - 2, .y = y } }, {}) catch unreachable;
|
|
} else {}
|
|
if (y + 2 < data.len and data[y + 2][x] == '.') {
|
|
set.put(Cheat{ .start = Point{ .x = x, .y = y }, .end = Point{ .x = x, .y = y + 2 } }, {}) catch unreachable;
|
|
}
|
|
if (y >= 2 and data[y - 2][x] == '.') {
|
|
set.put(Cheat{ .start = Point{ .x = x, .y = y }, .end = Point{ .x = x, .y = y - 2 } }, {}) catch unreachable;
|
|
}
|
|
}
|
|
}
|
|
|
|
var output = std.ArrayList(Cheat).init(allocator);
|
|
var set_iter = set.keyIterator();
|
|
while (set_iter.next()) |p| {
|
|
output.append(p.*) catch unreachable;
|
|
}
|
|
return output.toOwnedSlice() catch unreachable;
|
|
}
|
|
|
|
fn scoreCheats(cheats: []Cheat, base_lowest_time: u32, distances_from_start: std.AutoHashMap(Point, u32), distances_from_end: std.AutoHashMap(Point, u32), debug: bool, allocator: std.mem.Allocator) std.AutoHashMap(Cheat, i64) {
|
|
var output = std.AutoHashMap(Cheat, i64).init(allocator);
|
|
for (cheats) |cheat| {
|
|
output.put(cheat, scoreCheat(cheat, base_lowest_time, distances_from_start, distances_from_end, debug)) catch unreachable;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
// Note - `i64`, rather than `u32`, because a cheat might make the time _longer_!
|
|
fn scoreCheat(cheat: Cheat, base_lowest_time: u32, distances_from_start: std.AutoHashMap(Point, u32), distances_from_end: std.AutoHashMap(Point, u32), debug: bool) i64 {
|
|
const distance_from_start = distances_from_start.get(cheat.start).?;
|
|
const distance_from_end = distances_from_end.get(cheat.end).?;
|
|
const total_time_with_cheat = distance_from_start + distance_from_end + 2;
|
|
log("DEBUG - total_time_with_cheat for {s} is {} - based on {} and {}\n", .{ cheat, total_time_with_cheat, distance_from_start, distance_from_end }, debug);
|
|
return @as(i64, base_lowest_time) - @as(i64, total_time_with_cheat);
|
|
}
|
|
|
|
test "partOne" {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
const response = try partOne(true, true, allocator);
|
|
print("Found {} sufficiently-speedy cheats for partOne\n", .{response});
|
|
try expect(response == 8);
|
|
}
|
|
|
|
fn partTwo(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);
|
|
|
|
var 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');
|
|
// Cleanup the map so that cheats-to-the-end will still be legal
|
|
map[end_point.y][end_point.x] = '.';
|
|
map[start_point.y][start_point.x] = '.';
|
|
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;
|
|
|
|
var distances_map = util.dijkstra([][]u8, Point, &map, neighboursFunc, start_point, null, debug, allocator);
|
|
defer distances_map.deinit();
|
|
|
|
var distances_map_from_end = util.dijkstra([][]u8, Point, &map, neighboursFunc, end_point, null, debug, allocator);
|
|
defer distances_map_from_end.deinit();
|
|
|
|
const shortest_non_cheating_path = distances_map.get(end_point).?;
|
|
var cheats = findCheatsWithMaximumLength(map, 20, allocator);
|
|
defer cheats.deinit();
|
|
|
|
var scored_cheats = scoreCheatsWithVariableLength(cheats, shortest_non_cheating_path, distances_map, distances_map_from_end, debug, allocator);
|
|
defer scored_cheats.deinit();
|
|
|
|
var total: u32 = 0;
|
|
var it = scored_cheats.iterator();
|
|
const target_speedup: u8 = if (is_test_case) 66 else 100;
|
|
log("Here are the scored cheats:\n", .{}, debug);
|
|
while (it.next()) |e| {
|
|
if (e.value_ptr.* >= target_speedup) {
|
|
total += 1;
|
|
}
|
|
if (e.value_ptr.* > 0) {
|
|
log("{}: {}\n", .{ e.key_ptr.*, e.value_ptr.* }, debug);
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
fn findCheatsWithMaximumLength(map: [][]u8, maximum_length: u32, allocator: std.mem.Allocator) std.AutoHashMap(Cheat, u64) {
|
|
var output = std.AutoHashMap(Cheat, u64).init(allocator);
|
|
// Just realized - after running this - that I could probably speed this up by only iterating over the Points that are in the distance maps,
|
|
// since those are the only valid (non-wall) spaces. Eh - still only took a coupla seconds. Could add that optimization if it mattered!
|
|
for (map, 0..) |start_line, start_y| {
|
|
for (start_line, 0..) |start_c, start_x| {
|
|
for (map, 0..) |end_line, end_y| {
|
|
for (end_line, 0..) |end_c, end_x| {
|
|
if (start_c == '.' and end_c == '.') {
|
|
const cheat = Cheat{ .start = Point{ .x = start_x, .y = start_y }, .end = Point{ .x = end_x, .y = end_y } };
|
|
const length_of_cheat = findLengthOfCheat(cheat);
|
|
if (length_of_cheat <= maximum_length) {
|
|
output.put(cheat, length_of_cheat) catch unreachable;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
fn findLengthOfCheat(cheat: Cheat) u64 {
|
|
return @as(u64, (if (cheat.start.x > cheat.end.x) cheat.start.x - cheat.end.x else cheat.end.x - cheat.start.x) + (if (cheat.start.y > cheat.end.y) cheat.start.y - cheat.end.y else cheat.end.y - cheat.start.y));
|
|
}
|
|
|
|
fn scoreCheatsWithVariableLength(cheats: std.AutoHashMap(Cheat, u64), shortest_non_cheating_path: u32, distances_map: std.AutoHashMap(Point, u32), distances_map_from_end: std.AutoHashMap(Point, u32), debug: bool, allocator: std.mem.Allocator) std.AutoHashMap(Cheat, i128) {
|
|
var output = std.AutoHashMap(Cheat, i128).init(allocator);
|
|
var e_it = cheats.iterator();
|
|
while (e_it.next()) |e| {
|
|
output.put(e.key_ptr.*, scoreCheatWithVariableLength(e.key_ptr.*, e.value_ptr.*, shortest_non_cheating_path, distances_map, distances_map_from_end, debug)) catch unreachable;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
// Note - `i128`, rather than `u32`, because a cheat might make the time _longer_!
|
|
// (And we can't use i64 because the variable cheat-length is a usize, which is u64, so...:shrug:
|
|
fn scoreCheatWithVariableLength(cheat: Cheat, cheat_length: u64, base_lowest_time: u32, distances_from_start: std.AutoHashMap(Point, u32), distances_from_end: std.AutoHashMap(Point, u32), debug: bool) i128 {
|
|
const distance_from_start = distances_from_start.get(cheat.start).?;
|
|
const distance_from_end = distances_from_end.get(cheat.end).?;
|
|
const total_time_with_cheat = distance_from_start + distance_from_end + cheat_length;
|
|
log("DEBUG - total_time_with_cheat for {s} is {} - based on {} and {}\n", .{ cheat, total_time_with_cheat, distance_from_start, distance_from_end }, debug);
|
|
return @as(i128, base_lowest_time) - @as(i128, total_time_with_cheat);
|
|
}
|
|
|
|
test "partTwo" {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
const response = try partTwo(true, true, allocator);
|
|
print("Found {} sufficiently-speedy cheats for partTwo\n", .{response});
|
|
try expect(response == 67);
|
|
}
|