diff --git a/src/AdventOfCode/Day16.cs b/src/AdventOfCode/Day16.cs index 501082c..5f96ca0 100644 --- a/src/AdventOfCode/Day16.cs +++ b/src/AdventOfCode/Day16.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using AdventOfCode.Utilities; @@ -10,142 +9,183 @@ namespace AdventOfCode /// public class Day16 { - public int Part1(string[] input) - { - char[,] grid = input.ToGrid(); + public int Part1(string[] input) => BeamGrid.Parse(input).EnergisedTiles((0, 0), Bearing.East); - return Simulate(grid, (0,0), Bearing.East); - } + public int Part2(string[] input) => BeamGrid.Parse(input).MaxEnergisedTiles(); - public int Part2(string[] input) + /// + /// A grid of reflectors and splitters for light beams + /// + /// Grid + /// Grid width + /// Grid height + private record BeamGrid(char[,] Grid, int Width, int Height) { - char[,] grid = input.ToGrid(); - - int max = int.MinValue; - - foreach (int x in Enumerable.Range(0, input[0].Length)) + private readonly Queue<(Point2D Point, Bearing Bearing)> queue = new(); + private readonly HashSet<(Point2D Point, Bearing Bearing)> seen = new(); + + /// + /// Parse the beam grid + /// + /// Input + /// Beam grid + public static BeamGrid Parse(IReadOnlyList input) { - int result = Simulate(grid, (x, 0), Bearing.South); - max = Math.Max(max, result); + var grid = input.ToGrid(); + int width = input[0].Length; + int height = input.Count; - result = Simulate(grid, (x, input.Length - 1), Bearing.North); - max = Math.Max(max, result); + return new BeamGrid(grid, width, height); } - foreach (int y in Enumerable.Range(0, input.Length)) + /// + /// Maximum energised tiles that can be achieved from all starting edge points + /// + /// Max energised tiles + public int MaxEnergisedTiles() => this.WalkEdges().Max(edge => this.EnergisedTiles(edge.Point, edge.Bearing)); + + /// + /// Calculate how many tiles would be energised by the light beam starting at the given point on the given bearing + /// + /// Start point + /// Start bearing + /// Number of energised tiles + public int EnergisedTiles(Point2D startPoint, Bearing startBearing) { - int result = Simulate(grid, (0, y), Bearing.East); - max = Math.Max(max, result); - - result = Simulate(grid, (input[0].Length - 1, y), Bearing.West); - max = Math.Max(max, result); - } - - return max; - } - - private static int Simulate(char[,] grid, Point2D start, Bearing startBearing) - { - Queue<(Point2D Point, Bearing Bearing)> queue = new(); - queue.Enqueue((start, startBearing)); - - HashSet<(Point2D Point, Bearing Bearing)> seen = new(); + this.queue.Enqueue((startPoint, startBearing)); - while (queue.Count > 0) - { - (Point2D point, Bearing bearing) = queue.Dequeue(); + while (this.queue.Count > 0) + { + (Point2D point, Bearing bearing) = this.queue.Dequeue(); - seen.Add((point, bearing)); + this.seen.Add((point, bearing)); - char current = grid[point.Y, point.X]; + char current = this.Grid[point.Y, point.X]; - foreach ((Point2D Point, Bearing Bearing) next in Next(current, point, bearing)) - { - if (next.Point.X < 0 || next.Point.X >= grid.GetLength(1) || next.Point.Y < 0 || next.Point.Y >= grid.GetLength(0)) + foreach (Bearing move in NextMoves(current, bearing)) { - // fell off the edge - continue; - } + (Point2D Point, Bearing Bearing) next = (point.Move(move), move); - if (!seen.Contains(next)) - { - queue.Enqueue(next); + if (this.InBounds(next.Point) && !this.seen.Contains(next)) + { + this.queue.Enqueue(next); + } } } - } - return seen.Select(pair => pair.Point).Distinct().Count(); - } + int energised = this.seen.Select(pair => pair.Point).Distinct().Count(); - private static IEnumerable<(Point2D Point, Bearing Bearing)> Next(char current, Point2D point, Bearing bearing) - { - switch (current) - { - case '.': - yield return (point.Move(bearing), bearing); - break; - case '-': - switch (bearing) - { - case Bearing.East or Bearing.West: - yield return (point.Move(bearing), bearing); - break; - case Bearing.North or Bearing.South: - yield return (point.Move(Bearing.East), Bearing.East); - yield return (point.Move(Bearing.West), Bearing.West); - break; - } + // TODO: Retain the seen list between calls so we can memoize future calls without re-simulating + this.seen.Clear(); - break; - case '|': - switch (bearing) - { - case Bearing.East or Bearing.West: - yield return (point.Move(Bearing.North), Bearing.North); - yield return (point.Move(Bearing.South), Bearing.South); - break; - case Bearing.North or Bearing.South: - yield return (point.Move(bearing), bearing); - break; - } + return energised; + } - break; - case '\\': - switch (bearing) - { - case Bearing.North: - yield return (point.Move(Bearing.West), Bearing.West); - break; - case Bearing.South: - yield return (point.Move(Bearing.East), Bearing.East); - break; - case Bearing.East: - yield return (point.Move(Bearing.South), Bearing.South); - break; - case Bearing.West: - yield return (point.Move(Bearing.North), Bearing.North); - break; - } + /// + /// Calculate where the light beam will move next after hitting the current tile on the given bearing + /// + /// The beam can split if it hits a splitter, hence why more than one result can be returned + /// + /// Current tile + /// Current bearing + /// Bearing(s) of the light beam continuation(s) + private static IEnumerable NextMoves(char current, Bearing bearing) + { + switch (current) + { + case '.': + yield return bearing; + break; + case '-': + switch (bearing) + { + case Bearing.East or Bearing.West: + yield return bearing; + break; + case Bearing.North or Bearing.South: + yield return Bearing.East; + yield return Bearing.West; + break; + } + + break; + case '|': + switch (bearing) + { + case Bearing.East or Bearing.West: + yield return Bearing.North; + yield return Bearing.South; + break; + case Bearing.North or Bearing.South: + yield return bearing; + break; + } + + break; + case '\\': + switch (bearing) + { + case Bearing.North: + yield return Bearing.West; + break; + case Bearing.South: + yield return Bearing.East; + break; + case Bearing.East: + yield return Bearing.South; + break; + case Bearing.West: + yield return Bearing.North; + break; + } + + break; + case '/': + switch (bearing) + { + case Bearing.North: + yield return Bearing.East; + break; + case Bearing.South: + yield return Bearing.West; + break; + case Bearing.East: + yield return Bearing.North; + break; + case Bearing.West: + yield return Bearing.South; + break; + } + + break; + } + } - break; - case '/': - switch (bearing) - { - case Bearing.North: - yield return (point.Move(Bearing.East), Bearing.East); - break; - case Bearing.South: - yield return (point.Move(Bearing.West), Bearing.West); - break; - case Bearing.East: - yield return (point.Move(Bearing.North), Bearing.North); - break; - case Bearing.West: - yield return (point.Move(Bearing.South), Bearing.South); - break; - } + /// + /// Check if the given point is in bounds + /// + /// Point + /// Point is in bounds + private bool InBounds(Point2D point) => point.X >= 0 && point.X < this.Width + && point.Y >= 0 && point.Y < this.Height; + + /// + /// Get all the edge locations along with their inward pointing bearing + /// + /// Edges + private IEnumerable<(Point2D Point, Bearing Bearing)> WalkEdges() + { + foreach (int x in Enumerable.Range(0, this.Width)) + { + yield return ((x, 0), Bearing.South); + yield return ((x, this.Height - 1), Bearing.North); + } - break; + foreach (int y in Enumerable.Range(0, this.Height)) + { + yield return ((0, y), Bearing.East); + yield return ((this.Width - 1, y), Bearing.West); + } } } } diff --git a/src/AdventOfCode/Utilities/GridUtilities.cs b/src/AdventOfCode/Utilities/GridUtilities.cs index c50db3e..a195cba 100644 --- a/src/AdventOfCode/Utilities/GridUtilities.cs +++ b/src/AdventOfCode/Utilities/GridUtilities.cs @@ -23,12 +23,12 @@ private static readonly (int x, int y)[] Deltas = /// /// Input /// Char grid - public static char[,] ToGrid(this string[] input) + public static char[,] ToGrid(this IReadOnlyList input) { // y,x remember, not x,y - char[,] grid = new char[input.Length, input[0].Length]; + char[,] grid = new char[input.Count, input[0].Length]; - for (int y = 0; y < input.Length; y++) + for (int y = 0; y < input.Count; y++) { for (int x = 0; x < input[y].Length; x++) {