aoc-2024/day6.zig
2024-12-09 16:20:46 +01:00

452 lines
11 KiB
Zig

const std = @import("std");
const utils = @import("lib/utils.zig");
const filename = "inputs/day6.txt";
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
{
const file_reader = try utils.FileReader.init(alloc, filename);
defer file_reader.deinit();
const result = try part1(alloc, file_reader.reader());
try std.io.getStdOut().writer().print("Day 6, part 1: {}\n", .{result});
}
{
const file_reader = try utils.FileReader.init(alloc, filename);
defer file_reader.deinit();
const result = try part2(alloc, file_reader.reader());
try std.io.getStdOut().writer().print("Day 6, part 2: {}\n", .{result});
}
}
const Orientation = enum {
up,
right,
down,
left,
};
const MapElem = enum {
empty,
obstacle,
guard,
visited,
};
const GuardPos = struct {
x: i32,
y: i32,
orientation: Orientation,
};
const Map = [][]MapElem;
const MapState = struct {
alloc: std.mem.Allocator,
size_x: usize,
size_y: usize,
map: Map,
guard_pos: GuardPos,
pub fn format(self: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
_ = fmt;
_ = options;
for (self.map) |line| {
for (line) |elem| {
const c: u8 = switch (elem) {
.empty => '.',
.guard => switch (self.guard_pos.orientation) {
.up => '^',
.down => 'v',
.left => '<',
.right => '>',
},
.obstacle => '#',
.visited => 'X',
};
try writer.print("{c}", .{c});
}
try writer.print("\n", .{});
}
}
fn deinit(self: MapState) void {
for (self.map) |map_line| {
self.alloc.free(map_line);
}
self.alloc.free(self.map);
}
fn copyFrom(self: *MapState, other: MapState) void {
for (self.map, other.map) |*map_line, map_line_orig| {
@memcpy(map_line.*, map_line_orig);
}
}
fn clone(self: MapState, alloc: std.mem.Allocator) !MapState {
const map_orig = self.map;
const map = try alloc.alloc([]MapElem, map_orig.len);
for (map, map_orig) |*map_line, map_line_orig| {
map_line.* = try alloc.alloc(MapElem, map_line_orig.len);
}
var new_map_state = MapState{
.alloc = alloc,
.size_x = self.size_x,
.size_y = self.size_y,
.guard_pos = self.guard_pos,
.map = map,
};
new_map_state.copyFrom(self);
return new_map_state;
}
fn parse(alloc: std.mem.Allocator, reader: anytype) !MapState {
var line_reader = utils.lineReader(alloc, reader);
defer line_reader.deinit();
var map = std.ArrayList([]MapElem).init(alloc);
var guard_pos: GuardPos = undefined;
var x: usize = 0;
while (try line_reader.next()) |line| : (x += 1) {
const map_line = try alloc.alloc(MapElem, line.len);
for (0.., line, map_line) |y, c, *map_elem| {
map_elem.* = switch (c) {
'.' => .empty,
'#' => .obstacle,
'^' => blk: {
guard_pos = GuardPos{
.x = @intCast(x),
.y = @intCast(y),
.orientation = .up,
};
break :blk .guard;
},
else => unreachable,
};
}
try map.append(map_line);
}
const map_slice = try map.toOwnedSlice();
map.deinit();
const size_x = map_slice.len;
const size_y = map_slice[0].len;
for (map_slice) |map_line| {
std.debug.assert(map_line.len == size_y);
}
return .{
.alloc = alloc,
.size_x = size_x,
.size_y = size_y,
.map = map_slice,
.guard_pos = guard_pos,
};
}
fn get(self: MapState, x: i32, y: i32) MapElem {
std.debug.assert(0 <= x and x < (self.size_x));
std.debug.assert(0 <= y and y < (self.size_y));
return self.map[@intCast(x)][@intCast(y)];
}
fn set(self: *MapState, x: i32, y: i32, elem: MapElem) void {
std.debug.assert(0 <= x and x < (self.size_x));
std.debug.assert(0 <= y and y < (self.size_y));
self.map[@intCast(x)][@intCast(y)] = elem;
}
fn turn(self: *MapState) void {
self.guard_pos.orientation = switch (self.guard_pos.orientation) {
.up => .right,
.right => .down,
.down => .left,
.left => .up,
};
}
fn nextPos(self: MapState) ?struct { x: i32, y: i32 } {
const guard_pos = self.guard_pos;
const x = guard_pos.x;
const y = guard_pos.y;
var new_x: i32 = undefined;
var new_y: i32 = undefined;
switch (guard_pos.orientation) {
.up => {
new_x = x - 1;
new_y = y;
if (new_x < 0) {
return null;
}
},
.right => {
new_x = x;
new_y = y + 1;
if (new_y >= self.size_y) {
return null;
}
},
.down => {
new_x = x + 1;
new_y = y;
if (new_x >= self.size_x) {
return null;
}
},
.left => {
new_x = x;
new_y = y - 1;
if (new_y < 0) {
return null;
}
},
}
return .{ .x = new_x, .y = new_y };
}
fn advance(self: *MapState) enum { not_done, done } {
const guard_pos = &self.guard_pos;
const x = guard_pos.x;
const y = guard_pos.y;
if (self.get(x, y) != .guard) {
std.debug.print("x: {} y: {}\n", .{ x, y });
std.debug.print("map on failure:\n{}\n", .{self});
}
std.debug.assert(self.get(x, y) == .guard);
const new_pos = self.nextPos() orelse {
// walked out of the map
std.debug.assert(self.get(x, y) == .guard);
self.set(x, y, .visited);
return .done;
};
const new_x = new_pos.x;
const new_y = new_pos.y;
if (self.get(new_x, new_y) == .obstacle) {
// found obstacle: turn and advance
self.turn();
return advance(self);
}
std.debug.assert(self.get(x, y) == .guard);
self.set(x, y, .visited);
guard_pos.x = new_x;
guard_pos.y = new_y;
self.set(new_x, new_y, .guard);
return .not_done;
}
fn countVisited(self: MapState) u64 {
var count: u64 = 0;
for (self.map) |line| {
for (line) |elem| {
std.debug.assert(elem != .guard);
if (elem == .visited) {
count += 1;
}
}
}
return count;
}
};
fn part1(alloc: std.mem.Allocator, reader: anytype) !u64 {
var map_arena = std.heap.ArenaAllocator.init(alloc);
defer map_arena.deinit();
var map_state = try MapState.parse(map_arena.allocator(), reader);
while (map_state.advance() != .done) {}
const visited_count = map_state.countVisited();
return visited_count;
}
fn part2(alloc: std.mem.Allocator, reader: anytype) !u64 {
var map_arena = std.heap.ArenaAllocator.init(alloc);
defer map_arena.deinit();
const map_state_orig = try MapState.parse(map_arena.allocator(), reader);
var map_state_iter = try map_state_orig.clone(map_arena.allocator());
var tmp_arena = std.heap.ArenaAllocator.init(alloc);
defer tmp_arena.deinit();
var num_loops: u64 = 0;
var map_state_all_obstacles = try map_state_orig.clone(alloc);
defer map_state_all_obstacles.deinit();
var guard_pos_set = std.AutoHashMap(GuardPos, void).init(alloc);
defer guard_pos_set.deinit();
while (map_state_iter.advance() != .done) {
// current guard pos after last step
const cur_guard_pos = map_state_iter.guard_pos;
const cur_x = cur_guard_pos.x;
const cur_y = cur_guard_pos.y;
if (cur_x == map_state_orig.guard_pos.x and cur_y == map_state_orig.guard_pos.y) {
// don't put obstacle at initial position!
continue;
}
if (map_state_all_obstacles.get(cur_x, cur_y) == .obstacle) {
// we already tried this one
continue;
}
// clone original map state
var map_state_modified = try map_state_orig.clone(tmp_arena.allocator());
// put a new obstacle at the current position of the guard
// since obstacles only influence the path of the guard if she would step on,
// conversely the only relevant positions for new obstacles are the tiles she actually
// steps on
map_state_modified.set(cur_x, cur_y, .obstacle);
map_state_all_obstacles.set(cur_x, cur_y, .obstacle);
guard_pos_set.clearRetainingCapacity();
try guard_pos_set.put(map_state_modified.guard_pos, {});
while (map_state_modified.advance() != .done) {
// std.debug.print("modified map:\n{}\n", .{map_state_modified});
const guard_pos = map_state_modified.guard_pos;
if (guard_pos_set.contains(guard_pos)) {
num_loops += 1;
break;
}
try guard_pos_set.put(map_state_modified.guard_pos, {});
}
_ = tmp_arena.reset(.retain_capacity);
}
return num_loops;
}
test "part1 example" {
const alloc = std.testing.allocator;
const example =
\\....#.....
\\.........#
\\..........
\\..#.......
\\.......#..
\\..........
\\.#..^.....
\\........#.
\\#.........
\\......#...
;
var stream = std.io.fixedBufferStream(example);
const result = try part1(alloc, stream.reader());
try std.testing.expectEqual(41, result);
}
test "part1 input" {
const alloc = std.testing.allocator;
const file_reader = try utils.FileReader.init(alloc, filename);
defer file_reader.deinit();
const result = try part1(alloc, file_reader.reader());
try std.testing.expectEqual(4722, result);
}
test "part2 example" {
const alloc = std.testing.allocator;
const example =
\\....#.....
\\.........#
\\..........
\\..#.......
\\.......#..
\\..........
\\.#..^.....
\\........#.
\\#.........
\\......#...
;
var stream = std.io.fixedBufferStream(example);
const result = try part2(alloc, stream.reader());
try std.testing.expectEqual(6, result);
}
test "part2 input" {
const alloc = std.testing.allocator;
const file_reader = try utils.FileReader.init(alloc, filename);
defer file_reader.deinit();
const result = try part2(alloc, file_reader.reader());
try std.testing.expectEqual(1602, result);
}