From 7d15b70c2d0fd03e8fd970605e1cfa13a293c56d Mon Sep 17 00:00:00 2001 From: Jeremy Kaplan Date: Mon, 16 Dec 2024 19:53:44 -0800 Subject: [PATCH] Implement real single-source shortest-paths --- 2024/src/aoc.zig | 70 ++++++++++++++++++++++ 2024/src/day16.zig | 144 ++++++++++++++++++++------------------------- 2 files changed, 134 insertions(+), 80 deletions(-) diff --git a/2024/src/aoc.zig b/2024/src/aoc.zig index e5824cd..d114c64 100644 --- a/2024/src/aoc.zig +++ b/2024/src/aoc.zig @@ -123,3 +123,73 @@ pub fn readAll(allocator: std.mem.Allocator, path: []const u8) ![]u8 { return try file.readToEndAlloc(allocator, comptime 1 << 30); } + +pub fn shortestPath( + comptime State: type, + allocator: std.mem.Allocator, + ctx: State.Context, + start: State, +) !?u64 { + const Entry = struct { + state: State, + cost: u64, + + const Self = @This(); + + fn compare(_: void, a: Self, b: Self) std.math.Order { + return std.math.order(a.cost, b.cost); + } + }; + + var queue = std.PriorityQueue(Entry, void, comptime Entry.compare).init(allocator, {}); + defer queue.deinit(); + + try queue.add(Entry{ + .state = start, + .cost = 0, + }); + + var prev = std.AutoHashMap(State, State).init(allocator); + defer prev.deinit(); + + var dist = std.AutoHashMap(State, u64).init(allocator); + defer dist.deinit(); + + try dist.put(start, 0); + + while (queue.removeOrNull()) |entry| { + const u, const cost = .{ entry.state, entry.cost }; + if (dist.get(u)) |best| { + if (best < cost) { + continue; + } + } + + if (u.isGoal(ctx)) { + return cost; + } + + var neighbors: std.ArrayList(State.Neighbor) = try u.neighbors(ctx); + defer neighbors.deinit(); + + for (neighbors.items) |neighbor| { + const v, const extra = .{ neighbor.next, neighbor.extra }; + const alt = cost + extra; + + if (dist.get(v)) |dv| { + if (alt >= dv) { + continue; + } + } + + try prev.put(v, u); + try dist.put(v, alt); + try queue.add(Entry{ + .state = v, + .cost = alt, + }); + } + } + + return null; +} diff --git a/2024/src/day16.zig b/2024/src/day16.zig index e42684d..cfe9dd6 100644 --- a/2024/src/day16.zig +++ b/2024/src/day16.zig @@ -28,7 +28,7 @@ pub fn main() !void { } fn part1(allocator: Allocator, grid: Grid) !u64 { - const start, const goal = v: { + const pos, const goal = v: { var start: Pos = undefined; var end: Pos = undefined; @@ -46,93 +46,77 @@ fn part1(allocator: Allocator, grid: Grid) !u64 { const State = struct { pos: Pos, dir: Direction, - }; - - const Entry = struct { - path: List(State), - cost: u64, const Self = @This(); - fn compare(_: void, a: Self, b: Self) std.math.Order { - return std.math.order(a.cost, b.cost); + pub const Context = struct { + allocator: Allocator, + grid: Grid, + goal: Pos, + }; + + pub fn isGoal(self: Self, ctx: Context) bool { + return std.meta.eql(self.pos, ctx.goal); + } + + pub const Neighbor = struct { + next: Self, + extra: u64, + }; + + pub fn neighbors(self: Self, ctx: Context) !List(Neighbor) { + var list = List(Neighbor).init(ctx.allocator); + + // Move once in the direction we're already going. + { + const move = self.pos.move(self.dir); + if (ctx.grid.get(move) != '#') { + try list.append(Neighbor{ + .next = Self{ + .pos = move, + .dir = self.dir, + }, + .extra = 1, + }); + } + } + + // There's never any reason to turn unless we're going to immediately + // move forward. This keeps the search from every trying to spin in a circle. + // + // As a consequence, there's never *ever* a reason to turn twice in a row + // and go back the way we came in. + for ([_]Direction{ self.dir.clockwise(), self.dir.counterclockwise() }) |dir| { + const move = self.pos.move(dir); + if (ctx.grid.get(move) == '#') { + continue; + } + + try list.append(Neighbor{ + .next = Self{ + .pos = move, + .dir = dir, + }, + .extra = 1000 + 1, + }); + } + + return list; } }; - var queue = PriorityQueue(Entry, void, comptime Entry.compare).init(allocator, {}); - defer queue.deinit(); - defer while (queue.removeOrNull()) |*entry| entry.path.deinit(); + const start = State{ + .pos = pos, + .dir = Direction.e, + }; - try queue.add(v: { - var path = List(State).init(allocator); - try path.append(State{ - .pos = start, - .dir = Direction.e, - }); - break :v Entry{ - .path = path, - .cost = 0, - }; - }); + const ctx = State.Context{ + .allocator = allocator, + .grid = grid, + .goal = goal, + }; - var expanded = Set(State).init(allocator); - defer expanded.deinit(); - - while (queue.removeOrNull()) |entry| { - const path, const cost = .{ entry.path, entry.cost }; - defer path.deinit(); - - const state = path.getLast(); - - if (expanded.contains(state)) { - continue; - } - try expanded.put(state); - - if (std.meta.eql(state.pos, goal)) { - return cost; - } - - // Move once in the direction we're already going. - { - const move = state.pos.move(state.dir); - if (grid.get(move) != '#') { - var next = try path.clone(); - try next.append(State{ - .pos = move, - .dir = state.dir, - }); - try queue.add(Entry{ - .path = next, - .cost = cost + 1, - }); - } - } - - // There's never any reason to turn unless we're going to immediately - // move forward. This keeps the search from every trying to spin in a circle. - // - // As a consequence, there's never *ever* a reason to turn twice in a row - // and go back the way we came in. - for ([_]Direction{ state.dir.clockwise(), state.dir.counterclockwise() }) |dir| { - const move = state.pos.move(dir); - if (grid.get(move) == '#') { - continue; - } - - var next = try path.clone(); - try next.append(State{ - .pos = move, - .dir = dir, - }); - try queue.add(Entry{ - .path = next, - .cost = cost + 1000 + 1, - }); - } - } - - return 0; + return (try aoc.shortestPath(State, allocator, ctx, start)).?; } const Pos = struct {