Add day 16
This commit is contained in:
289
Days/Day16.cs
Normal file
289
Days/Day16.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user