diff --git a/.run/Day11 example.run.xml b/.run/Day11 example.run.xml new file mode 100644 index 0000000..e8e5f5e --- /dev/null +++ b/.run/Day11 example.run.xml @@ -0,0 +1,21 @@ + + + + diff --git a/.run/Day11.run.xml b/.run/Day11.run.xml new file mode 100644 index 0000000..f8a87b6 --- /dev/null +++ b/.run/Day11.run.xml @@ -0,0 +1,20 @@ + + + + diff --git a/AdventOfCode2023.sln b/AdventOfCode2023.sln index c362b4e..dbefd4f 100644 --- a/AdventOfCode2023.sln +++ b/AdventOfCode2023.sln @@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Day09", "Day09 - Mirage Mai EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Day10", "Day10 - Pipe Maze\Day10.csproj", "{D237B79E-9D78-4F99-B9BA-43B52609AAAE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Day11", "Day11 - Cosmic Expansion\Day11.csproj", "{28DC51A6-39A7-4180-B890-D5B5D0AF71EC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -92,6 +94,10 @@ Global {D237B79E-9D78-4F99-B9BA-43B52609AAAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {D237B79E-9D78-4F99-B9BA-43B52609AAAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {D237B79E-9D78-4F99-B9BA-43B52609AAAE}.Release|Any CPU.Build.0 = Release|Any CPU + {28DC51A6-39A7-4180-B890-D5B5D0AF71EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28DC51A6-39A7-4180-B890-D5B5D0AF71EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28DC51A6-39A7-4180-B890-D5B5D0AF71EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28DC51A6-39A7-4180-B890-D5B5D0AF71EC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CE8086B8-A3E4-40E0-859F-607F95D25514} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26} @@ -104,5 +110,6 @@ Global {78787B01-0E1E-472E-8B58-4FFD864FC1F6} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26} {1E0917F9-35B3-4474-B335-E72C6BDFE667} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26} {D237B79E-9D78-4F99-B9BA-43B52609AAAE} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26} + {28DC51A6-39A7-4180-B890-D5B5D0AF71EC} = {F6C05A63-6269-425F-B877-9E9F0BC1FC26} EndGlobalSection EndGlobal diff --git a/Day11 - Cosmic Expansion/.vscode/launch.json b/Day11 - Cosmic Expansion/.vscode/launch.json new file mode 100644 index 0000000..3fe2df0 --- /dev/null +++ b/Day11 - Cosmic Expansion/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch (my input)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/net8.0/Day11.dll", + "args": [ + "input.txt" + ], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + } + ] +} diff --git a/Day11 - Cosmic Expansion/.vscode/tasks.json b/Day11 - Cosmic Expansion/.vscode/tasks.json new file mode 100644 index 0000000..283437e --- /dev/null +++ b/Day11 - Cosmic Expansion/.vscode/tasks.json @@ -0,0 +1,21 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "group": { + "kind": "build", + "isDefault": true + }, + "args": [ + "build", + "${workspaceFolder}/Day11.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/Day11 - Cosmic Expansion/Day11.csproj b/Day11 - Cosmic Expansion/Day11.csproj new file mode 100644 index 0000000..3541b0c --- /dev/null +++ b/Day11 - Cosmic Expansion/Day11.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + AdventOfCode.Year2023.Day11 + + + + + + + + + + Always + + + + diff --git a/Day11 - Cosmic Expansion/Day11Solver.cs b/Day11 - Cosmic Expansion/Day11Solver.cs new file mode 100644 index 0000000..832c59d --- /dev/null +++ b/Day11 - Cosmic Expansion/Day11Solver.cs @@ -0,0 +1,63 @@ +using AdventOfCode.Abstractions; +using AdventOfCode.Common.Geometry; +using AdventOfCode.Year2023.Day11.Puzzle; + +namespace AdventOfCode.Year2023.Day11; + +public sealed class Day11Solver : DaySolver +{ + public override int Year => 2023; + public override int Day => 11; + public override string Title => "Cosmic Expansion"; + + private readonly IReadOnlyCollection _initialPositions; + private readonly Day11SolverOptions _options; + + public Day11Solver(Day11SolverOptions options) : base(options) + { + _options = options; + var inputReader = new InputReader(options.GalaxyChar); + _initialPositions = inputReader.ReadGalaxyPositions(Input); + } + + public Day11Solver(Action configure) + : this(DaySolverOptions.FromConfigureAction(configure)) + { + } + + public Day11Solver() : this(new Day11SolverOptions()) + { + } + + public override string SolvePart1() + { + var galaxyMap = new GalaxyMap(_initialPositions); + galaxyMap.Expand(_options.PartOneExpansionMagnitude); + long sum = SumDistancesBetweenGalaxies(galaxyMap); + return sum.ToString(); + } + + public override string SolvePart2() + { + var galaxyMap = new GalaxyMap(_initialPositions); + galaxyMap.Expand(_options.PartTwoExpansionMagnitude); + long sum = SumDistancesBetweenGalaxies(galaxyMap); + return sum.ToString(); + } + + private static long SumDistancesBetweenGalaxies(GalaxyMap galaxyMap) + { + long sum = 0; + for (int i = 0; i < galaxyMap.Galaxies.Count; i++) + { + var g1 = galaxyMap.Galaxies[i]; + for (int j = i + 1; j < galaxyMap.Galaxies.Count; j++) + { + var g2 = galaxyMap.Galaxies[j]; + sum += MathG.ManhattanDistance(g1.Position, g2.Position); + } + } + + return sum; + } +} diff --git a/Day11 - Cosmic Expansion/Day11SolverOptions.cs b/Day11 - Cosmic Expansion/Day11SolverOptions.cs new file mode 100644 index 0000000..cd40471 --- /dev/null +++ b/Day11 - Cosmic Expansion/Day11SolverOptions.cs @@ -0,0 +1,11 @@ +using AdventOfCode.Abstractions; + +namespace AdventOfCode.Year2023.Day11; + +public sealed class Day11SolverOptions : DaySolverOptions +{ + public char GalaxyChar { get; set; } = '#'; + + public int PartOneExpansionMagnitude { get; set; } = 2; + public int PartTwoExpansionMagnitude { get; set; } = 1_000_000; +} diff --git a/Day11 - Cosmic Expansion/GlobalUsings.cs b/Day11 - Cosmic Expansion/GlobalUsings.cs new file mode 100644 index 0000000..4ceafc3 --- /dev/null +++ b/Day11 - Cosmic Expansion/GlobalUsings.cs @@ -0,0 +1 @@ +global using Point = AdventOfCode.Common.Geometry.Point2D; diff --git a/Day11 - Cosmic Expansion/Program.cs b/Day11 - Cosmic Expansion/Program.cs new file mode 100644 index 0000000..0f22d07 --- /dev/null +++ b/Day11 - Cosmic Expansion/Program.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; +using AdventOfCode; +using AdventOfCode.Year2023.Day11; + +try +{ + string? filepath = args.Length switch + { + 0 => null, + 1 => args[0], + _ => throw new CommandLineException( + $"Program was called with too many arguments. Proper usage: \"dotnet run []\"." + ) + }; + + Day11Solver solver = new(options => { options.InputFilepath = filepath ?? options.InputFilepath; }); + + Console.WriteLine($"--- Day {solver.Day}: {solver.Title} ---"); + + Console.Write("Part one: "); + string part1 = solver.SolvePart1(); + Console.WriteLine(part1); + + Console.Write("Part two: "); + string part2 = solver.SolvePart2(); + Console.WriteLine(part2); +} +catch (AdventOfCodeException e) +{ + string errorPrefix = e switch + { + CommandLineException => "Command line error", + InputException => "Input error", + DaySolverException => "Day solver error", + _ => throw new UnreachableException($"Unknown exception type \"{e.GetType()}\".") + }; + + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine($"{errorPrefix}: {e.Message}"); + Console.ResetColor(); + Environment.Exit(1); +} diff --git a/Day11 - Cosmic Expansion/Puzzle/Galaxy.cs b/Day11 - Cosmic Expansion/Puzzle/Galaxy.cs new file mode 100644 index 0000000..58436f9 --- /dev/null +++ b/Day11 - Cosmic Expansion/Puzzle/Galaxy.cs @@ -0,0 +1,11 @@ +namespace AdventOfCode.Year2023.Day11.Puzzle; + +internal interface IGalaxy +{ + Point Position { get; } +} + +internal sealed class Galaxy(Point position) : IGalaxy +{ + public Point Position { get; set; } = position; +} diff --git a/Day11 - Cosmic Expansion/Puzzle/GalaxyMap.cs b/Day11 - Cosmic Expansion/Puzzle/GalaxyMap.cs new file mode 100644 index 0000000..dabb39b --- /dev/null +++ b/Day11 - Cosmic Expansion/Puzzle/GalaxyMap.cs @@ -0,0 +1,95 @@ +namespace AdventOfCode.Year2023.Day11.Puzzle; + +internal sealed class GalaxyMap +{ + private const int ExpandListInitialCapacity = 10; + + private readonly List _galaxies; + private long _maxColumn; + private long _maxRow; + + public GalaxyMap(IEnumerable galaxyPositions) + { + _galaxies = galaxyPositions.Select(p => new Galaxy(p)).ToList(); + _maxRow = _galaxies.Max(g => g.Position.X); + _maxColumn = _galaxies.Max(g => g.Position.Y); + } + + public IReadOnlyList Galaxies => _galaxies; + + public void Expand(int expansionMagnitude) + { + ExpandRows(expansionMagnitude); + ExpandColumns(expansionMagnitude); + } + + private void ExpandRows(int expansionMagnitude) + { + var galaxiesPerRow = new List?[_maxRow + 1]; + foreach (var galaxy in _galaxies) + { + long row = galaxy.Position.X; + var rowList = galaxiesPerRow[row]; + if (rowList is null) + { + rowList = new List(ExpandListInitialCapacity); + galaxiesPerRow[row] = rowList; + } + + rowList.Add(galaxy); + } + + int rowOffset = 0; + for (int row = 0; row <= _maxRow; row++) + { + var rowList = galaxiesPerRow[row]; + if (rowList is null) + { + rowOffset = rowOffset + expansionMagnitude - 1; + continue; + } + + foreach (var galaxy in rowList) + { + galaxy.Position = new(row + rowOffset, galaxy.Position.Y); + } + } + + _maxRow += rowOffset; + } + + private void ExpandColumns(int expansionMagnitude) + { + var galaxiesPerColumn = new List?[_maxColumn + 1]; + foreach (var galaxy in _galaxies) + { + long column = galaxy.Position.Y; + var columnList = galaxiesPerColumn[column]; + if (columnList is null) + { + columnList = new List(ExpandListInitialCapacity); + galaxiesPerColumn[column] = columnList; + } + + columnList.Add(galaxy); + } + + int columnOffset = 0; + for (int column = 0; column <= _maxColumn; column++) + { + var columnList = galaxiesPerColumn[column]; + if (columnList is null) + { + columnOffset = columnOffset + expansionMagnitude - 1; + continue; + } + + foreach (var galaxy in columnList) + { + galaxy.Position = new(galaxy.Position.X, column + columnOffset); + } + } + + _maxColumn += columnOffset; + } +} diff --git a/Day11 - Cosmic Expansion/Puzzle/InputReader.cs b/Day11 - Cosmic Expansion/Puzzle/InputReader.cs new file mode 100644 index 0000000..36fd4fd --- /dev/null +++ b/Day11 - Cosmic Expansion/Puzzle/InputReader.cs @@ -0,0 +1,35 @@ +namespace AdventOfCode.Year2023.Day11.Puzzle; + +internal sealed class InputReader +{ + private const int InitialGalaxyListSize = 500; + private readonly char _galaxyChar; + + public InputReader(char galaxyChar) + { + _galaxyChar = galaxyChar; + } + + public IReadOnlyCollection ReadGalaxyPositions(string input) + { + var list = new List(InitialGalaxyListSize); + int row = 0; + foreach (var lineSpan in input.AsSpan().EnumerateLines()) + { + int column = 0; + foreach (char c in lineSpan) + { + if (c == _galaxyChar) + { + list.Add(new(row, column)); + } + + column++; + } + + row++; + } + + return list; + } +} diff --git a/Day11 - Cosmic Expansion/README.md b/Day11 - Cosmic Expansion/README.md new file mode 100644 index 0000000..f0f33c6 --- /dev/null +++ b/Day11 - Cosmic Expansion/README.md @@ -0,0 +1,5 @@ +# [Day 11: Cosmic Expansion](https://adventofcode.com/2023/day/11) + +In this Advent of Code challenge, you help a researcher studying cosmic expansion by finding the sum of the lengths of +the shortest paths between pairs of galaxies as represented in a grid. +Cosmic expansion causes empty rows and columns to double in size, complicating path calculations. diff --git a/Tests/Day11Tests.cs b/Tests/Day11Tests.cs new file mode 100644 index 0000000..b894e4d --- /dev/null +++ b/Tests/Day11Tests.cs @@ -0,0 +1,33 @@ +using AdventOfCode.Year2023.Day11; + +namespace AdventOfCode.Year2023.Tests; + +[Trait("Year", "2023")] +[Trait("Day", "11")] +public sealed class Day11Tests : BaseDayTests +{ + protected override string DayInputsDirectory => "Day11"; + + protected override Day11Solver CreateSolver(Day11SolverOptions options) => new(options); + + [Theory] + [InlineData("example-input.txt", "374")] + [InlineData("my-input.txt", "10173804")] + public void TestPart1(string inputFilename, string expectedResult) + => BaseTestPart1(inputFilename, expectedResult); + + [Theory] + [InlineData("example-input.txt", "1030", 10)] + [InlineData("example-input.txt", "8410", 100)] + [InlineData("my-input.txt", "634324905172")] + public void TestPart2(string inputFilename, string expectedResult, int? expansionMagnitude = null) + { + var options = new Day11SolverOptions(); + if (expansionMagnitude.HasValue) + { + options.PartTwoExpansionMagnitude = expansionMagnitude.Value; + } + + BaseTestPart2(inputFilename, expectedResult, options); + } +} diff --git a/Tests/Inputs/Day11/example-input.txt b/Tests/Inputs/Day11/example-input.txt new file mode 100644 index 0000000..c7c12d4 --- /dev/null +++ b/Tests/Inputs/Day11/example-input.txt @@ -0,0 +1,10 @@ +...#...... +.......#.. +#......... +.......... +......#... +.#........ +.........# +.......... +.......#.. +#...#..... diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 37e4b90..335cbf3 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -40,6 +40,7 @@ +