diff --git a/src/AdventOfCode/Day14.cs b/src/AdventOfCode/Day14.cs index ac543b8..2d34d35 100644 --- a/src/AdventOfCode/Day14.cs +++ b/src/AdventOfCode/Day14.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Text; using AdventOfCode.Utilities; @@ -15,7 +14,7 @@ public class Day14 public int Part1(string[] input) { Map map = Map.Parse(input); - map.Move(Bearing.North); + map.Tilt(Bearing.North); return map.Load; } @@ -23,53 +22,69 @@ public int Part2(string[] input) { Map map = Map.Parse(input); - List loads = new(); + List loads = new(256); + Dictionary seen = new(256); - for (int i = 0; i < 1000; i++) + int loopStart; + + for (int i = 0; ; i++) { map.Cycle(); - int load = map.Load; - - loads.Add(load); - - //Debug.WriteLine(map.Print()); - } - - // 102921 -- too high - // 102188 -- too high + string state = map.Print(); - Dictionary analysis = loads.GroupBy(x => x).ToDictionary(x => x.Key, x => x.Count()); - - // 101400 -- too high - // 101288 -- incorrect (5min lockout, no too high/low info) - - var keys = analysis.Where(kvp => kvp.Value > 1).OrderBy(kvp => kvp.Key); + if (seen.TryGetValue(state, out loopStart)) + { + break; + } - int cycleLength = keys.Count(); + loads.Add(map.Load); + seen[state] = i; + } - int index = ((1_000_000_000 - (analysis.Count - cycleLength)) % cycleLength) + analysis.Count - 1; + int loopLength = seen.Count - loopStart; + int index = (1_000_000_000 - loopStart) % loopLength + loopStart - 1; return loads[index]; } + /// + /// Map of rocks and walls + /// private class Map { - private readonly ISet walls; - private readonly IList balls; + private readonly HashSet walls; + private readonly List balls; private readonly int height; private readonly int width; + private readonly StringBuilder printer; + /// + /// The load on the north wall of the map + /// public int Load => this.balls.Select(b => this.height - b.Y).Sum(); - private Map(ISet walls, IList balls, int height, int width) + /// + /// Initialises a new instance of the class. + /// + /// Fixed wall positions + /// Mobile ball positions + /// Map height + /// Map width + private Map(HashSet walls, List balls, int height, int width) { this.walls = walls; this.balls = balls; this.height = height; this.width = width; + this.printer = new StringBuilder(this.width * this.height + this.height * Environment.NewLine.Length); } + /// + /// Parse the input + /// + /// Input + /// Parsed map public static Map Parse(IReadOnlyList input) { HashSet walls = new(); @@ -90,22 +105,22 @@ public static Map Parse(IReadOnlyList input) return new Map(walls, balls, input.Count, input[0].Length); } + /// + /// Perform one full tilting cycle on the map + /// public void Cycle() { - this.Move(Bearing.North); - //Debug.WriteLine(this.Print()); - - this.Move(Bearing.West); - //Debug.WriteLine(this.Print()); - - this.Move(Bearing.South); - //Debug.WriteLine(this.Print()); - - this.Move(Bearing.East); - //Debug.WriteLine(this.Print()); + this.Tilt(Bearing.North); + this.Tilt(Bearing.West); + this.Tilt(Bearing.South); + this.Tilt(Bearing.East); } - public void Move(Bearing bearing) + /// + /// Tile the map in the given direction to move all of the balls + /// + /// Tilt direction + public void Tilt(Bearing bearing) { IEnumerable ordered = this.Order(bearing); HashSet settled = new(); @@ -121,20 +136,18 @@ public void Move(Bearing bearing) next = current.Move(bearing); } - bool added = settled.Add(current); - Debug.Assert(added); + settled.Add(current); } - Debug.Assert(this.balls.Count == settled.Count, "We've lost some balls..."); - this.balls.Clear(); - - foreach (Point2D ball in settled) - { - this.balls.Add(ball); - } + this.balls.AddRange(settled); } + /// + /// Order the balls so they can move correctly depending on the tilt direction + /// + /// Tilt direction + /// Ordered balls to move private IEnumerable Order(Bearing bearing) => bearing switch { Bearing.North => this.balls.OrderBy(b => b.Y), @@ -144,6 +157,12 @@ public void Move(Bearing bearing) _ => throw new ArgumentOutOfRangeException(nameof(bearing), bearing, null) }; + /// + /// Check if the given point is still in bounds according to the tilt direction + /// + /// Point + /// Tilt direction + /// Ball is still in bounds private bool InBounds(Point2D point, Bearing bearing) => bearing switch { Bearing.North => point.Y > 0, @@ -152,9 +171,13 @@ public void Move(Bearing bearing) Bearing.West => point.X > 0 }; + /// + /// Print the current state of the map + /// + /// Map state public string Print() { - StringBuilder s = new(this.width * this.height + this.height * Environment.NewLine.Length); + this.printer.Clear(); HashSet settled = this.balls.ToHashSet(); for (int y = 0; y < this.height; y++) @@ -163,22 +186,22 @@ public string Print() { if (this.walls.Contains((x, y))) { - s.Append('#'); + this.printer.Append('#'); } else if (settled.Contains((x, y))) { - s.Append('O'); + this.printer.Append('O'); } else { - s.Append('.'); + this.printer.Append('.'); } } - s.AppendLine(); + this.printer.AppendLine(); } - return s.ToString(); + return this.printer.ToString(); } } }