using System.Numerics; using Spectre.Console; namespace AdventOfCode.Days; public class Day16 : Day { public override int Number => 16; public override string Name => "Reindeer Maze"; private const int Size = 141; private const char Wall = '#'; private const char Empty = '.'; private const char Start = 'S'; private const char End = 'E'; private static readonly Point DirectionUp = new(0, -1); private static readonly Point DirectionDown = new(0, 1); private static readonly Point DirectionLeft = new(-1, 0); private static readonly Point DirectionRight = new(1, 0); public override void RunPart1(bool display = true) { var (maze, start, end) = ParseMaze(); var visited = new Dictionary<(Point Position, Point Direction), int>(); var minimumScore = int.MaxValue; var toVisit = new Queue<(Point Position, Point Direction, int Score)>(); toVisit.Enqueue((start, DirectionRight, 0)); while (toVisit.Count > 0) { var (position, direction, score) = toVisit.Dequeue(); if (visited.TryGetValue((position, direction), out var savedScore) && savedScore <= score) { continue; } visited[(position, direction)] = score; // Reached end if (position == end) { if (score < minimumScore) { minimumScore = score; } continue; } // Try forward if there is no wall var destination = position + direction; if (maze[destination.X, destination.Y] is not Wall) { toVisit.Enqueue((destination, direction, score + 1)); } // Also try changing direction toVisit.Enqueue((position, NextDirection(direction), score + 1000)); toVisit.Enqueue((position, PreviousDirection(direction), score + 1000)); } if (display) { AnsiConsole.MarkupLine($"[green]Lowest score: [yellow]{minimumScore}[/][/]"); } } public override void RunPart2(bool display = true) { var (maze, start, end) = ParseMaze(); var visited = new Dictionary<(Point Position, Point Direction), int>(); var minimumScore = int.MaxValue; var toVisit = new Queue<(Point Position, Point Direction, int Score)>(); toVisit.Enqueue((start, DirectionRight, 0)); // First we need to find minimum score while (toVisit.Count > 0) { var (position, direction, score) = toVisit.Dequeue(); if (visited.TryGetValue((position, direction), out var savedScore) && savedScore <= score) { continue; } visited[(position, direction)] = score; // Reached end if (position == end) { if (score < minimumScore) { minimumScore = score; } continue; } // Try forward if there is no wall var destination = position + direction; if (maze[destination.X, destination.Y] is not Wall) { toVisit.Enqueue((destination, direction, score + 1)); } // Also try changing direction toVisit.Enqueue((position, NextDirection(direction), score + 1000)); toVisit.Enqueue((position, PreviousDirection(direction), score + 1000)); } // Now that we have minimum score, we need to find all the paths that lead to this score var toVisitPath = new Stack<(Point Position, Point Direction, int Score, List Path)>(); toVisitPath.Push((start, DirectionRight, 0, [])); var minimumScorePaths = new List>(); while (toVisitPath.Count > 0) { var (position, direction, score, path) = toVisitPath.Pop(); if (score > minimumScore) { continue; } if (visited.TryGetValue((position, direction), out var savedScore) && savedScore < score) { continue; } path.Add(position); // Reached end if (position == end) { minimumScorePaths.Add(path); continue; } // Try forward if there is no wall var destination = position + direction; if (maze[destination.X, destination.Y] is not Wall) { toVisitPath.Push((destination, direction, score + 1, path)); } // Also try changing direction toVisitPath.Push((position, NextDirection(direction), score + 1000, path.ToList())); toVisitPath.Push((position, PreviousDirection(direction), score + 1000, path.ToList())); } if (display) { AnsiConsole.MarkupLine($"[green]Unique positions in optimal paths: [yellow]{minimumScorePaths.SelectMany(l => l).ToHashSet().Count}[/][/]"); } } private (char[,] Maze, Point Start, Point end) ParseMaze() { var maze = new char[Size, Size]; Point start = default; Point end = default; var y = 0; foreach (var line in Input.AsSpan().EnumerateLines()) { var x = 0; foreach (var symbol in line) { if (symbol is Start) { start = new Point(x, y); maze[x, y] = Empty; } else if (symbol is End) { end = new Point(x, y); maze[x, y] = Empty; } else { maze[x, y] = symbol; } x++; } y++; } return (maze, start, end); } private static Point NextDirection(Point direction) { if (direction == DirectionUp) { return DirectionRight; } if (direction == DirectionRight) { return DirectionDown; } if (direction == DirectionDown) { return DirectionLeft; } if (direction == DirectionLeft) { return DirectionUp; } throw new ArgumentException("Invalid direction", nameof(direction)); } private static Point PreviousDirection(Point direction) { if (direction == DirectionUp) { return DirectionLeft; } if (direction == DirectionLeft) { return DirectionDown; } if (direction == DirectionDown) { return DirectionRight; } if (direction == DirectionRight) { return DirectionUp; } throw new ArgumentException("Invalid direction", nameof(direction)); } private readonly record struct Point(int X, int Y) : IAdditionOperators, ISubtractionOperators, IMultiplyOperators { public static Point operator +(Point left, Point right) { return new Point(left.X + right.X, left.Y + right.Y); } public static Point operator -(Point left, Point right) { return new Point(left.X - right.X, left.Y - right.Y); } public static Point operator *(Point left, int right) { return new Point(left.X * right, left.Y * right); } public static Point operator *(int left, Point right) { return new Point(right.X * left, right.Y * left); } public static implicit operator Point((int X, int Y) point) => new(point.X, point.Y); } }