289 lines
7.9 KiB
C#
289 lines
7.9 KiB
C#
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<Point> Path)>();
|
|
toVisitPath.Push((start, DirectionRight, 0, []));
|
|
|
|
var minimumScorePaths = new List<List<Point>>();
|
|
|
|
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<Point, Point, Point>,
|
|
ISubtractionOperators<Point, Point, Point>,
|
|
IMultiplyOperators<Point, int, Point>
|
|
{
|
|
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);
|
|
}
|
|
} |