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 _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(); var distanceFromStart = new Dictionary(); 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(); var distanceFromStart = new Dictionary(); 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, 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); public int DistanceTo(Point other) { var distance = other - this; return Math.Abs(distance.X) + Math.Abs(distance.Y); } } }