2025-01-23 21:13:47 -08:00

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);
}