Solution to 18-2

This commit is contained in:
Jack Jackson 2025-01-20 12:09:16 -08:00
parent 6914093f48
commit 6a8d9325e0

View File

@ -16,7 +16,7 @@ pub fn main() !void {
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const response = try partOne(false, false, allocator);
const response = try partTwo(false, false, allocator);
print("{}\n", .{response});
}
@ -155,12 +155,211 @@ fn buildMap(dimension: usize, allocator: std.mem.Allocator) [][]u8 {
return map_list.toOwnedSlice() catch unreachable;
}
test "partOne" {
// test "partOne" {
// var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// defer _ = gpa.deinit();
// const allocator = gpa.allocator();
// const response = try partOne(true, true, allocator);
// print("Response is {}\n", .{response});
// try expect(response == 22);
// }
// Outline of logic:
// * (Without any bytes fallen) find the shortest path. Make sure you note the _path_, not just the length
// * Repeatedly:
// * Let bytes fall until one falls _on_ the shortest-path
// * Calculate the new shortest path. If none exists, the last byte that fell is the answer
//
// ...having implemented this, I now realize there's probably a faster way to do this - add bytes until there is a
// single path _on blocked bytes_ from the left-and-bottom edges to top-and-right edges (allowing for diagonals). Ah
// well - this reuses already-written code!
fn partTwo(is_test_case: bool, debug: bool, allocator: std.mem.Allocator) !Point {
if (debug) {
print("DEBUG LOGGING IS ENABLED\n", .{});
}
const dimension: usize = if (is_test_case) 7 else 71;
var map = buildMap(dimension, allocator);
defer allocator.free(map);
defer {
for (map) |line| {
allocator.free(line);
}
}
const input_file = try util.getInputFile("18", is_test_case);
const data = try util.readAllInputWithAllocator(input_file, allocator);
var data_it = std.mem.splitScalar(u8, data, '\n');
defer allocator.free(data);
var bytes_fallen: usize = 0;
var last_fallen_byte: Point = undefined;
while (true) {
std.time.sleep(1000000000);
const maybe_shortest_path = findShortestPath(map, dimension, debug, allocator);
if (maybe_shortest_path) |shortest_path| {
// Ditto below - want this to show up even if we have debug off for Dijkstra
print("Found a shortest path: ", .{});
var path_it = shortest_path.keyIterator();
while (path_it.next()) |point| {
print("{s},", .{point});
}
print("\n", .{});
while (data_it.next()) |line| : (bytes_fallen += 1) {
const comma_index = std.mem.indexOf(u8, line, ",").?;
const x = std.fmt.parseInt(u8, line[0..comma_index], 10) catch unreachable;
const y = std.fmt.parseInt(u8, line[comma_index + 1 ..], 10) catch unreachable;
const byte_point = Point{ .x = x, .y = y };
last_fallen_byte = byte_point;
map[y][x] = '#';
print("Blocking {s} in map\n", .{byte_point});
if (shortest_path.contains(byte_point)) {
// This falling byte has caused a shortest-path to be blocked - recalculate
// This could arguably be `log`, but I want to see this even without the debug-logging of the Dijkstra
print("After the fall of the {}-th byte ({s}), the shortest path was blocked. Recalculating\n", .{ bytes_fallen + 1, byte_point });
bytes_fallen += 1;
// There _must_ be a better way to do this!? But without an intermediate variable, `shortest_path`
// is `*const`, meaning it can't be `deinit`ed
var pointer_to_shortest_path = shortest_path;
pointer_to_shortest_path.deinit();
break;
}
}
} else {
// No shortest-path could be found
print("Could not find a shortest path\n", .{});
break;
}
}
return last_fallen_byte;
}
// Technically this finds "the set of Points that are on _a_ shortest-path" - which is fine for our purposes, as we're
// only trying to (eventually) find the case where _all_ paths are cut off. So, a byte that cuts off _any_ shortest-path
// is valid to trigger a recalculation, even if the other shortest-path is still valid - we'll just recalculate to find
// that one (then continue).
fn findShortestPath(map: [][]const u8, dimension: usize, debug: bool, allocator: std.mem.Allocator) ?std.AutoHashMap(Point, void) {
if (true) { // Should really be `if debug` but we don't have enough specificity in that.
print("Finding shortest path in the following map:\n", .{});
for (map) |line| {
for (line) |c| {
print("{c}", .{c});
}
print("\n", .{});
}
}
var visited = std.AutoHashMap(Point, void).init(allocator);
defer visited.deinit();
var distances = std.AutoHashMap(Point, u32).init(allocator);
defer distances.deinit();
distances.put(Point{ .x = 0, .y = 0 }, 0) catch unreachable;
var candidates = std.AutoHashMap(Point, void).init(allocator);
candidates.put(Point{ .x = 0, .y = 0 }, {}) catch unreachable;
defer candidates.deinit();
const length_of_path = while (true) {
var cand_it = candidates.keyIterator();
var current_point: Point = undefined;
var lowest_distance_found: u32 = std.math.maxInt(u32);
while (cand_it.next()) |cand| {
const actual_point = cand.*;
log("Considering {s} as the next current_point ", .{actual_point}, debug);
if (visited.contains(actual_point)) {
log("but rejecting it because it's been visited already\n", .{}, debug);
continue;
}
const distance_of_candidate = distances.get(actual_point) orelse std.math.maxInt(u32);
if (distance_of_candidate < lowest_distance_found) {
log("and it is a possibility!\n", .{}, debug);
current_point = actual_point;
lowest_distance_found = distance_of_candidate;
} else {
log("but rejecting it because it already has a shorter minimum-distance ({} vs. {})\n", .{ distance_of_candidate, lowest_distance_found }, debug);
}
}
log("Found point {s} as current_point - it has distance {}\n", .{ current_point, lowest_distance_found }, debug);
if (lowest_distance_found == std.math.maxInt(u32)) {
print("ERROR - iterated over all candidates, but found none with non-infinite distance\n", .{});
break lowest_distance_found;
}
// Check for termination condition
if (current_point.x == dimension - 1 and current_point.y == dimension - 1) {
break lowest_distance_found;
}
// Haven't terminated => we're still on a non-target node. Check neighbours, and update their min-distance.
const distance_of_neighbour_from_current_point = lowest_distance_found + 1;
const neighbours = current_point.neighbours(dimension, dimension, allocator);
for (neighbours) |neighbour| {
if (visited.contains(neighbour)) {
log("Not processing {s} because it's already been visited\n", .{neighbour}, debug);
continue;
}
if (map[neighbour.y][neighbour.x] == '.') {
const distance_response = distances.getOrPut(neighbour) catch unreachable;
if (!distance_response.found_existing) {
log("Adding a new (first) distance to {s} (via {s}) - {}\n", .{ neighbour, current_point, distance_of_neighbour_from_current_point }, debug);
distance_response.value_ptr.* = distance_of_neighbour_from_current_point;
} else {
if (distance_response.value_ptr.* > distance_of_neighbour_from_current_point) {
log("Overriding distance for neighbour {s} because distance of path from {s} ({}) is less than current value ({})\n", .{ neighbour, current_point, distance_of_neighbour_from_current_point, distance_response.value_ptr.* }, debug);
distance_response.value_ptr.* = distance_of_neighbour_from_current_point;
}
}
log("Adding {s} to candidates\n", .{neighbour}, debug);
candidates.put(neighbour, {}) catch unreachable;
} else {
log("Not processing neighbour {s} because it is blocked off\n", .{neighbour}, debug);
}
}
allocator.free(neighbours);
// Update sets for next iteration
visited.put(current_point, {}) catch unreachable;
// Not strictly necessary, as we already check for visited whenever iterating over candidates - but this will
// prevent some unnecessary double-processing.
_ = candidates.remove(current_point);
};
if (length_of_path == std.math.maxInt(u32)) {
// No path exists
return null;
}
// At this point, `distances` contains enough distances to trace a shortest-path - now just have to traceback.
var shortest_path = std.AutoHashMap(Point, void).init(allocator);
var current_point: Point = Point{ .x = dimension - 1, .y = dimension - 1 };
var current_distance = length_of_path;
while (!(current_point.x == 0 and current_point.y == 0)) {
shortest_path.put(current_point, {}) catch unreachable;
const neighbours = current_point.neighbours(dimension, dimension, allocator);
for (neighbours) |neighbour| {
if (distances.get(neighbour) orelse std.math.maxInt(u32) == current_distance - 1) {
current_point = neighbour;
current_distance -= 1;
break;
}
}
allocator.free(neighbours);
}
shortest_path.put(Point{ .x = 0, .y = 0 }, {}) catch unreachable;
return shortest_path;
}
test "partTwo" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const response = try partOne(true, true, allocator);
const response = try partTwo(true, false, allocator);
print("Response is {}\n", .{response});
try expect(response == 22);
try expect(response.x == 6 and response.y == 1);
}