Files
AdventOfCode/Days/Day20.cs
2025-01-24 10:19:13 +01:00

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);
}
}
}