288 lines
8.6 KiB
C#
288 lines
8.6 KiB
C#
using System.Collections.Frozen;
|
|
using System.Numerics;
|
|
using System.Runtime.Intrinsics.X86;
|
|
using Spectre.Console;
|
|
|
|
namespace AdventOfCode.Days;
|
|
|
|
public class Day20 : Day
|
|
{
|
|
public override int Number => 20;
|
|
public override string Name => "Race Condition";
|
|
|
|
private const int Size = 141;
|
|
private const char Wall = '#';
|
|
private const char Empty = '.';
|
|
|
|
private readonly List<Point> _cheatOffsets =
|
|
[
|
|
(2, 0),
|
|
(1, 1),
|
|
(0, 2),
|
|
(-1, 1),
|
|
(-2, 0),
|
|
(-1, -1),
|
|
(0, -2),
|
|
(1, -1)
|
|
];
|
|
|
|
public override void RunPart1(bool display = true)
|
|
{
|
|
var (map, start, end) = ParseMap();
|
|
|
|
// Find track path
|
|
var trackPath = new List<Point>();
|
|
var distanceFromStart = new Dictionary<Point, int>();
|
|
|
|
var position = start;
|
|
|
|
var distance = 0;
|
|
while (position != end)
|
|
{
|
|
trackPath.Add(position);
|
|
distanceFromStart[position] = distance;
|
|
|
|
// Find next track position
|
|
distance++;
|
|
var up = position + new Point(0, -1);
|
|
if (up is { X: >= 0 and < Size, Y: >= 0 and < Size } && map[up.X, up.Y] is not Wall && !distanceFromStart.ContainsKey(up))
|
|
{
|
|
position = up;
|
|
|
|
continue;
|
|
}
|
|
|
|
var down = position + new Point(0, 1);
|
|
if (down is { X: >= 0 and < Size, Y: >= 0 and < Size } && map[down.X, down.Y] is not Wall && !distanceFromStart.ContainsKey(down))
|
|
{
|
|
position = down;
|
|
|
|
continue;
|
|
}
|
|
|
|
var left = position + new Point(-1, 0);
|
|
if (left is { X: >= 0 and < Size, Y: >= 0 and < Size } && map[left.X, left.Y] is not Wall && !distanceFromStart.ContainsKey(left))
|
|
{
|
|
position = left;
|
|
|
|
continue;
|
|
}
|
|
|
|
var right = position + new Point(1, 0);
|
|
if (right is { X: >= 0 and < Size, Y: >= 0 and < Size } && map[right.X, right.Y] is not Wall && !distanceFromStart.ContainsKey(right))
|
|
{
|
|
position = right;
|
|
}
|
|
}
|
|
|
|
// Also add end
|
|
trackPath.Add(position);
|
|
distanceFromStart[position] = distance;
|
|
|
|
var cheats = new Dictionary<(Point StartPosition, Point EndPosition), int>();
|
|
|
|
// Now return to start and try all possible combination of cheats for each track position
|
|
foreach (var trackPosition in trackPath)
|
|
{
|
|
var originDistance = distanceFromStart[trackPosition];
|
|
|
|
foreach (var cheatOffset in _cheatOffsets)
|
|
{
|
|
var cheatDestination = trackPosition + cheatOffset;
|
|
|
|
if (cheatDestination.X is < 0 or >= Size || cheatDestination.Y is < 0 or >= Size)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (map[cheatDestination.X, cheatDestination.Y] is Wall)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var destinationDistance = distanceFromStart[cheatDestination];
|
|
var savedDistance = destinationDistance - originDistance - 2;
|
|
|
|
// Only take into account real shortcuts, not those that do not save time
|
|
if (savedDistance < 1)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
cheats.Add((trackPosition, cheatDestination), savedDistance);
|
|
}
|
|
}
|
|
|
|
if (display)
|
|
{
|
|
AnsiConsole.MarkupLine($"[green]Number of cheats that save at least 100 picoseconds: [yellow]{cheats.Count(c => c.Value >= 100)}[/][/]");
|
|
}
|
|
}
|
|
|
|
public override void RunPart2(bool display = true)
|
|
{
|
|
var (map, start, end) = ParseMap();
|
|
|
|
// Find track path
|
|
var trackPath = new List<Point>();
|
|
var distanceFromStart = new Dictionary<Point, int>();
|
|
|
|
var position = start;
|
|
|
|
var distance = 0;
|
|
while (position != end)
|
|
{
|
|
trackPath.Add(position);
|
|
distanceFromStart[position] = distance;
|
|
|
|
// Find next track position
|
|
distance++;
|
|
var up = position + new Point(0, -1);
|
|
if (up is { X: >= 0 and < Size, Y: >= 0 and < Size } && map[up.X, up.Y] is not Wall && !distanceFromStart.ContainsKey(up))
|
|
{
|
|
position = up;
|
|
|
|
continue;
|
|
}
|
|
|
|
var down = position + new Point(0, 1);
|
|
if (down is { X: >= 0 and < Size, Y: >= 0 and < Size } && map[down.X, down.Y] is not Wall && !distanceFromStart.ContainsKey(down))
|
|
{
|
|
position = down;
|
|
|
|
continue;
|
|
}
|
|
|
|
var left = position + new Point(-1, 0);
|
|
if (left is { X: >= 0 and < Size, Y: >= 0 and < Size } && map[left.X, left.Y] is not Wall && !distanceFromStart.ContainsKey(left))
|
|
{
|
|
position = left;
|
|
|
|
continue;
|
|
}
|
|
|
|
var right = position + new Point(1, 0);
|
|
if (right is { X: >= 0 and < Size, Y: >= 0 and < Size } && map[right.X, right.Y] is not Wall && !distanceFromStart.ContainsKey(right))
|
|
{
|
|
position = right;
|
|
}
|
|
}
|
|
|
|
// Also add end
|
|
trackPath.Add(position);
|
|
distanceFromStart[position] = distance;
|
|
|
|
var cheats = new Dictionary<(Point StartPosition, Point EndPosition), int>();
|
|
|
|
// Now return to start and try all possible combination of cheats for each track position
|
|
foreach (var trackPosition in trackPath)
|
|
{
|
|
var originDistance = distanceFromStart[trackPosition];
|
|
|
|
foreach (var cheatDestination in trackPath.Where(destination => trackPosition.DistanceTo(destination) <= 20))
|
|
{
|
|
var cheatLength = trackPosition.DistanceTo(cheatDestination);
|
|
|
|
var destinationDistance = distanceFromStart[cheatDestination];
|
|
var savedDistance = destinationDistance - originDistance - cheatLength;
|
|
|
|
// Only take into account real shortcuts, not those that do not save time
|
|
if (savedDistance < 1)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (cheats.TryGetValue((trackPosition, cheatDestination), out var otherSavedDistance))
|
|
{
|
|
if (savedDistance <= otherSavedDistance)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
cheats[(trackPosition, cheatDestination)] = savedDistance;
|
|
}
|
|
}
|
|
|
|
if (display)
|
|
{
|
|
AnsiConsole.MarkupLine($"[green]Number of cheats that save at least 100 picoseconds: [yellow]{cheats.Count(c => c.Value >= 100)}[/][/]");
|
|
}
|
|
}
|
|
|
|
private (char[,] Map, Point Start, Point End) ParseMap()
|
|
{
|
|
var map = 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 == 'S')
|
|
{
|
|
start = new Point(x, y);
|
|
}
|
|
else if (symbol == 'E')
|
|
{
|
|
end = new Point(x, y);
|
|
}
|
|
|
|
if (symbol is Wall)
|
|
{
|
|
map[x, y] = Wall;
|
|
}
|
|
else
|
|
{
|
|
map[x, y] = Empty;
|
|
}
|
|
|
|
x++;
|
|
}
|
|
|
|
y++;
|
|
}
|
|
|
|
return (map, start, end);
|
|
}
|
|
|
|
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);
|
|
|
|
public int DistanceTo(Point other)
|
|
{
|
|
var distance = other - this;
|
|
|
|
return Math.Abs(distance.X) + Math.Abs(distance.Y);
|
|
}
|
|
}
|
|
} |