diff --git a/src/AdventOfCode/Day22.cs b/src/AdventOfCode/Day22.cs new file mode 100644 index 0000000..7043b4c --- /dev/null +++ b/src/AdventOfCode/Day22.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AdventOfCode.Utilities; + +namespace AdventOfCode +{ + /// + /// Solver for Day 22 + /// + public class Day22 + { + public int Part1(string[] input) + { + var cubes = input.Select((line, i) => SandCube.Parse(i, line)).ToArray(); + var tower = SandCubeTower.Build(cubes); + + int total = 0; + + foreach (SandCube cube in cubes) + { + if (!tower.Supporting.TryGetValue(cube.Id, out ISet holdingUp)) + { + // cube doesn't support anything, safe to remove + total++; + continue; + } + + if (holdingUp.All(h => tower.SupportedBy[h].Count > 1)) + { + // we're supporting things, but everything we're supporting is itself supported by at least one other thing + total++; + } + } + + return total; + } + + public int Part2(string[] input) + { + var cubes = input.Select((line, i) => SandCube.Parse(i, line)).ToArray(); + var tower = SandCubeTower.Build(cubes); + + int total = 0; + + foreach (SandCube cube in cubes) + { + Queue queue = new(); + HashSet fell = new() { cube.Id }; + + queue.Enqueue(cube.Id); + + while (queue.Count > 0) + { + int id = queue.Dequeue(); + + if (!tower.Supporting.TryGetValue(id, out ISet holdingUp)) + { + // not holding anything up, so nothing to fall + continue; + } + + foreach (int heldUp in holdingUp) + { + if (tower.SupportedBy[heldUp].All(fell.Contains)) + { + // everything supporting this one fell down + queue.Enqueue(heldUp); + fell.Add(heldUp); + } + } + } + + total += fell.Count - 1; // don't count the block that we removed + } + + return total; + } + + /// + /// Tower of sand cubes + /// + /// Lookup of each sand cube to which other sand cubes it is supporting + /// Lookup of each sand cube to which other sand cubes it is supported by + private record SandCubeTower(IDictionary> Supporting, IDictionary> SupportedBy) + { + /// + /// Building the tower from the given starting cube positions representing falling cubes + /// + /// Falling cubes + /// Sand cube tower after all cubes have fallen and settled + public static SandCubeTower Build(IEnumerable falling) + { + Dictionary> supporting = new(); + Dictionary> supportedBy = new(); + Dictionary occupiedSpace = new(); + + foreach (SandCube cube in falling.OrderBy(c => c.BottomLeftFront.Z)) + { + SandCube current = cube; + SandCube dropped = cube.Drop(); + + // drop until we hit either the ground or a point occupied by another cube + while (dropped.BottomLayer().All(p => p.Z > 0 && !occupiedSpace.ContainsKey(p))) + { + current = dropped; + dropped = current.Drop(); + } + + // mark the cubes below as supporting this one + foreach (Point3D point in dropped.BottomLayer().Where(occupiedSpace.ContainsKey)) + { + int supportingId = occupiedSpace[point]; + supporting.GetOrCreate(supportingId, () => new HashSet()).Add(current.Id); + supportedBy.GetOrCreate(current.Id, () => new HashSet()).Add(supportingId); + } + + // settle this cube in place + foreach (Point3D point in current.Points()) + { + occupiedSpace[point] = current.Id; + } + } + + return new SandCubeTower(supporting, supportedBy); + } + } + + /// + /// A cube of sand + /// + /// Cube ID + /// The bottom front left corner + /// The top right back corner + private record SandCube(int Id, Point3D BottomLeftFront, Point3D TopRightBack) + { + /// + /// Parse a sand cube from input + /// + /// Cube ID + /// Input line + /// Sand cube + public static SandCube Parse(int id, string input) + { + int[] numbers = input.Numbers(); + + Point3D bottomLeftFront = (Math.Min(numbers[0], numbers[3]), + Math.Min(numbers[1], numbers[4]), + Math.Min(numbers[2], numbers[5])); + + Point3D topRightBack = (Math.Max(numbers[0], numbers[3]), + Math.Max(numbers[1], numbers[4]), + Math.Max(numbers[2], numbers[5])); + + return new SandCube(id, bottomLeftFront, topRightBack); + } + + /// + /// Drop the current cube by one place + /// + /// Same cube but space one lower down + public SandCube Drop() + { + return this with + { + BottomLeftFront = this.BottomLeftFront - (0, 0, 1), + TopRightBack = this.TopRightBack - (0, 0, 1) + }; + } + + /// + /// Enumerate all the points of space taken up by this cube + /// + /// Cube points + public IEnumerable Points() + { + for (int x = this.BottomLeftFront.X; x <= this.TopRightBack.X; x++) + { + for (int y = this.BottomLeftFront.Y; y <= this.TopRightBack.Y; y++) + { + for (int z = this.BottomLeftFront.Z; z <= this.TopRightBack.Z; z++) + { + yield return (x, y, z); + } + } + } + } + + /// + /// Enumerate all the points of space taken up by the bottom layer of this cube + /// + /// Bottom layer + public IEnumerable BottomLayer() + { + for (int x = this.BottomLeftFront.X; x <= this.TopRightBack.X; x++) + { + for (int y = this.BottomLeftFront.Y; y <= this.TopRightBack.Y; y++) + { + yield return (x, y, this.BottomLeftFront.Z); + } + } + } + } + } +} diff --git a/src/AdventOfCode/inputs/day22.txt b/src/AdventOfCode/inputs/day22.txt new file mode 100644 index 0000000..c38b88b Binary files /dev/null and b/src/AdventOfCode/inputs/day22.txt differ diff --git a/tests/AdventOfCode.Tests/Day22Tests.cs b/tests/AdventOfCode.Tests/Day22Tests.cs new file mode 100644 index 0000000..45fa637 --- /dev/null +++ b/tests/AdventOfCode.Tests/Day22Tests.cs @@ -0,0 +1,81 @@ +using System.IO; +using Xunit; +using Xunit.Abstractions; + + +namespace AdventOfCode.Tests +{ + public class Day22Tests + { + private readonly ITestOutputHelper output; + private readonly Day22 solver; + + public Day22Tests(ITestOutputHelper output) + { + this.output = output; + this.solver = new Day22(); + } + + private static string[] GetRealInput() + { + string[] input = File.ReadAllLines("inputs/day22.txt"); + return input; + } + + private static string[] GetSampleInput() + { + return new string[] + { + "1,0,1~1,2,1", + "0,0,2~2,0,2", + "0,2,3~2,2,3", + "0,0,4~0,2,4", + "2,0,5~2,2,5", + "0,1,6~2,1,6", + "1,1,8~1,1,9", + }; + } + + [Fact] + public void Part1_SampleInput_ProducesCorrectResponse() + { + var expected = 5; + + var result = solver.Part1(GetSampleInput()); + + Assert.Equal(expected, result); + } + + [Fact] + public void Part1_RealInput_ProducesCorrectResponse() + { + var expected = 395; + + var result = solver.Part1(GetRealInput()); + output.WriteLine($"Day 22 - Part 1 - {result}"); + + Assert.Equal(expected, result); + } + + [Fact] + public void Part2_SampleInput_ProducesCorrectResponse() + { + var expected = 7; + + var result = solver.Part2(GetSampleInput()); + + Assert.Equal(expected, result); + } + + [Fact] + public void Part2_RealInput_ProducesCorrectResponse() + { + var expected = 64714; + + var result = solver.Part2(GetRealInput()); + output.WriteLine($"Day 22 - Part 2 - {result}"); + + Assert.Equal(expected, result); + } + } +}