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 @@
+