Merge pull request #8 from StevanFreeborn/stevanfreeborn/feat/solve-puzzle-8

feat: solve puzzle 8
This commit is contained in:
Stevan Freeborn
2023-12-09 00:49:14 -06:00
committed by GitHub
9 changed files with 526 additions and 29 deletions
+9 -2
View File
@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Trebuchet;
@@ -23,9 +24,15 @@ public class Program
: new PartOnePuzzleSolver();
var input = await File.ReadAllLinesAsync(args[0]);
var stopwatch = new Stopwatch();
stopwatch.Start();
var result = puzzleSolver.SumCalibrationValues(input);
Console.WriteLine($"The sum of all calibration values is {result}.");
stopwatch.Stop();
Console.WriteLine($"The sum of all calibration values is {result}. ({stopwatch.ElapsedMilliseconds}ms)");
return result;
}
@@ -0,0 +1,2 @@
global using Xunit;
global using FluentAssertions;
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HauntedWasteland\HauntedWasteland.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\INPUT.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
+118
View File
@@ -0,0 +1,118 @@
namespace HauntedWasteland.Tests;
public class MapTests
{
[Theory]
[MemberData(nameof(TestData.ParseMapTestData), MemberType = typeof(TestData))]
public void Parse_WhenGivenStringArray_ItShouldReturnMap(string[] input, Map expected)
{
Map.Parse(input).Should().BeEquivalentTo(expected);
}
[Theory]
[MemberData(nameof(TestData.CountStepsTestData), MemberType = typeof(TestData))]
public void CountStepsToZ_WhenGivenMap_ItShouldReturnNumberOfSteps(string[] input, int expected)
{
Map.Parse(input).CountStepsToZ().Should().Be(expected);
}
[Theory]
[MemberData(nameof(TestData.CountStepsToAllZNodesData), MemberType = typeof(TestData))]
public void CountStepsToAllZNodes_WhenGivenMap_ItShouldReturnNumberOfSteps(string[] input, long expected)
{
Map.Parse(input).CountStepsToAllZNodes().Should().Be(expected);
}
[Fact]
public void FindPrimeFactors_WhenGivenNumber_ItShouldReturnPrimeFactors()
{
new Map([], []).FindPrimeFactors(11911).Should().BeEquivalentTo([43, 277]);
}
[Fact]
public void FindLeastCommonMultiple_WhenGivenNumbers_ItShouldReturnLeastCommonMultiple()
{
new Map([], []).FindLeastCommonMultiple([16343, 11911, 20221, 21883, 13019, 19667]).Should().Be(13524038372771);
}
public static class TestData
{
private static readonly string[] MapInput =
[
"RL",
"",
"AAA = (BBB, CCC)",
"BBB = (DDD, EEE)",
"CCC = (ZZZ, GGG)",
"DDD = (DDD, DDD)",
"EEE = (EEE, EEE)",
"GGG = (GGG, GGG)",
"ZZZ = (ZZZ, ZZZ)",
];
private static readonly string[] Input = File.ReadAllLines("INPUT.txt");
public static IEnumerable<object[]> CountStepsTestData =>
new List<object[]>
{
new object[]
{
MapInput,
2
},
new object[]
{
Input,
13019
}
};
public static IEnumerable<object[]> CountStepsToAllZNodesData =>
new List<object[]>
{
new object[]
{
new string[]
{
"LR",
"",
"11A = (11B, XXX)",
"11B = (XXX, 11Z)",
"11Z = (11B, XXX)",
"22A = (22B, XXX)",
"22B = (22C, 22C)",
"22C = (22Z, 22Z)",
"22Z = (22B, 22B)",
"XXX = (XXX, XXX)",
},
6
},
new object[]
{
Input,
13524038372771
}
};
public static IEnumerable<object[]> ParseMapTestData =>
new List<object[]>
{
new object[]
{
MapInput,
new Map(
['R', 'L'],
[
new("AAA", "BBB", "CCC"),
new("BBB", "DDD", "EEE"),
new("CCC", "ZZZ", "GGG"),
new("DDD", "DDD", "DDD"),
new("EEE", "EEE", "EEE"),
new("GGG", "GGG", "GGG"),
new("ZZZ", "ZZZ", "ZZZ"),
]
)
},
};
}
}
+54
View File
@@ -0,0 +1,54 @@
namespace HauntedWasteland.Tests;
public class NodeTests
{
[Theory]
[MemberData(nameof(TestData.ParseNodeTestData), MemberType = typeof(TestData))]
public void Parse_WhenGivenString_ItShouldReturnNode(string input, Node expected)
{
Node.Parse(input).Should().BeEquivalentTo(expected);
}
public static class TestData
{
public static IEnumerable<object[]> ParseNodeTestData =>
new List<object[]>
{
new object[]
{
"AAA = (BBB, CCC)",
new Node("AAA", "BBB", "CCC")
},
new object[]
{
"BBB = (DDD, EEE)",
new Node("BBB", "DDD", "EEE")
},
new object[]
{
"CCC = (ZZZ, GGG)",
new Node("CCC", "ZZZ", "GGG")
},
new object[]
{
"DDD = (DDD, DDD)",
new Node("DDD", "DDD", "DDD")
},
new object[]
{
"EEE = (EEE, EEE)",
new Node("EEE", "EEE", "EEE")
},
new object[]
{
"GGG = (GGG, GGG)",
new Node("GGG", "GGG", "GGG")
},
new object[]
{
"ZZZ = (ZZZ, ZZZ)",
new Node("ZZZ", "ZZZ", "ZZZ")
},
};
}
}
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+254
View File
@@ -0,0 +1,254 @@
using System.Diagnostics;
namespace HauntedWasteland;
public class Program
{
public static async Task<int> Main(string[] args)
{
if (args.Length is 0)
{
Console.WriteLine("Please provide a path to the input file.");
return -1;
}
if (File.Exists(args[0]) is false)
{
Console.WriteLine("The provided file does not exist.");
return -2;
}
var isPart2 = args.Length > 1 && args[1] == "part2";
var input = await File.ReadAllLinesAsync(args[0]);
var stopwatch = new Stopwatch();
stopwatch.Start();
var map = Map.Parse(input);
var result = isPart2
? map.CountStepsToAllZNodes()
: map.CountStepsToZ();
stopwatch.Stop();
Console.WriteLine($"The number of steps is {result}. ({stopwatch.ElapsedMilliseconds}ms)");
return (int)result;
}
}
/// <summary>
/// A map of the Haunted Wasteland.
/// </summary>
/// <param name="turns">The turns to take at each step.</param>
/// <param name="nodes">The nodes in the map.</param>
/// <returns>An instance of <see cref="Map"/>.</returns>
public class Map(
List<char> turns,
List<Node> nodes
)
{
/// <summary>
/// Gets the turns to take at each step.
/// </summary>
public List<char> Turns { get; init; } = turns;
/// <summary>
/// Gets the nodes in the map.
/// </summary>
public List<Node> Nodes { get; init; } = nodes;
/// <summary>
/// Parses a map from a string array.
/// </summary>
/// <param name="mapInput">The map input.</param>
/// <returns>An instance of <see cref="Map"/>.</returns>
public static Map Parse(string[] mapInput)
{
var turns = mapInput[0].ToList();
var nodes = mapInput[2..]
.Select(Node.Parse)
.ToList();
return new Map(turns, nodes);
}
/// <summary>
/// Counts the number of steps to the Z node.
/// </summary>
/// <returns>The number of steps to the Z node.</returns>
public int CountStepsToZ()
{
var current = Nodes.First(n => n.Current == "AAA");
var steps = 0;
while (current.Current != "ZZZ")
{
var next = Turns[steps % Turns.Count] switch
{
'R' => current.Right,
'L' => current.Left,
_ => throw new Exception("Invalid turn")
};
current = Nodes.First(n => n.Current == next);
steps++;
}
return steps;
}
/// <summary>
/// Finds the prime factors of a number.
/// </summary>
/// <param name="number">The number to find the prime factors of.</param>
/// <returns>The prime factors of the number.</returns>
public List<long> FindPrimeFactors(long number)
{
var factors = new List<long>();
// Start with the smallest prime number, 2.
var divisor = 2;
// Continue until the number is reduced to 2 or less.
while (number >= 2)
{
// If the number is divisible by the current divisor,
if (number % divisor == 0)
{
// Add the divisor to the list of factors.
factors.Add(divisor);
// Divide the number by the divisor to reduce it.
number /= divisor;
}
else
{
// If the number is not divisible by the current divisor, increment the divisor.
divisor++;
}
}
return factors;
}
/// <summary>
/// Finds the least common multiple of a list of numbers.
/// </summary>
/// <param name="numbers">The numbers to find the least common multiple of.</param>
/// <returns>The least common multiple of the numbers.</returns>
public long FindLeastCommonMultiple(List<long> numbers)
{
var primeFactors = numbers.Select(FindPrimeFactors).ToList();
var uniquePrimeFactors = primeFactors
.SelectMany(pf => pf)
.Distinct()
.ToList();
var maxPrimeFactors = uniquePrimeFactors
.Select(upf => primeFactors.Max(pf => pf.Count(f => f == upf)))
.ToList();
var result = uniquePrimeFactors
.Zip(maxPrimeFactors)
.Aggregate(
(long)1,
(acc, b) =>
// b.First is the prime factor
// b.Second is the number of times it occurs
acc * (long)Math.Pow(b.First, b.Second)
);
return result;
}
/// <summary>
/// Counts the number of steps to all Z nodes.
/// </summary>
/// <returns>The number of steps to all Z nodes.</returns>
public long CountStepsToAllZNodes()
{
var startNodes = Nodes.Where(n => n.Current.EndsWith('A')).ToList();
var nodeSteps = new List<long>();
foreach (var startNode in startNodes)
{
var current = startNode;
var steps = 0;
while (current.Current.EndsWith('Z') is false)
{
var next = Turns[steps % Turns.Count] switch
{
'R' => current.Right,
'L' => current.Left,
_ => throw new Exception("Invalid turn")
};
current = Nodes.First(n => n.Current == next);
steps++;
}
nodeSteps.Add(steps);
}
return FindLeastCommonMultiple(nodeSteps);
}
}
/// <summary>
/// A node in the map.
/// </summary>
/// <param name="current">The current node.</param>
/// <param name="left">The left node.</param>
/// <param name="right">The right node.</param>
/// <returns>An instance of <see cref="Node"/>.</returns>
public class Node(
string current,
string left,
string right
)
{
/// <summary>
/// Gets the current node.
/// </summary>
public string Current { get; init; } = current;
/// <summary>
/// Gets the left node.
/// </summary>
public string Left { get; init; } = left;
/// <summary>
/// Gets the right node.
/// </summary>
public string Right { get; init; } = right;
/// <summary>
/// Casts the node to a string.
/// </summary>
public override string ToString()
{
return $"{Current} = ({Left},{Right})";
}
/// <summary>
/// Parses a node from a string.
/// </summary>
/// <param name="nodeString">The node string.</param>
/// <returns>An instance of <see cref="Node"/>.</returns>
public static Node Parse(string nodeString)
{
var parts = nodeString.Split(
'=',
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries
);
var current = parts[0];
var nextNodes = parts[1].Split(
',',
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries
);
var left = nextNodes[0].Trim('(');
var right = nextNodes[1].Trim(')');
return new Node(current, left, right);
}
}
+16
View File
@@ -45,6 +45,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CamelCards", "07\CamelCards
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CamelCards.Tests", "07\CamelCards.Tests\CamelCards.Tests.csproj", "{8F04A78E-D2E1-41AC-B43C-74608B7B4FCB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "08", "08", "{F21CCF39-1F8A-40DA-A0E5-F1426B09BAA2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HauntedWasteland", "08\HauntedWasteland\HauntedWasteland.csproj", "{332B56C1-8ECB-4E34-9E69-54BF6A192CFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HauntedWasteland.Tests", "08\HauntedWasteland.Tests\HauntedWasteland.Tests.csproj", "{FA8DDDD1-4DD9-4927-A9FB-C4FACDEAF901}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -110,6 +116,14 @@ Global
{8F04A78E-D2E1-41AC-B43C-74608B7B4FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F04A78E-D2E1-41AC-B43C-74608B7B4FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F04A78E-D2E1-41AC-B43C-74608B7B4FCB}.Release|Any CPU.Build.0 = Release|Any CPU
{332B56C1-8ECB-4E34-9E69-54BF6A192CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{332B56C1-8ECB-4E34-9E69-54BF6A192CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{332B56C1-8ECB-4E34-9E69-54BF6A192CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{332B56C1-8ECB-4E34-9E69-54BF6A192CFB}.Release|Any CPU.Build.0 = Release|Any CPU
{FA8DDDD1-4DD9-4927-A9FB-C4FACDEAF901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA8DDDD1-4DD9-4927-A9FB-C4FACDEAF901}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA8DDDD1-4DD9-4927-A9FB-C4FACDEAF901}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA8DDDD1-4DD9-4927-A9FB-C4FACDEAF901}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3F8EAC09-4BC7-43AA-B72B-48DDD2710F6D} = {8C29858C-623A-461A-BF0B-254E151CD9C2}
@@ -126,5 +140,7 @@ Global
{8C89E1D0-2617-4B89-BA9C-9189FC69BC43} = {86704890-D6B8-4166-853B-F1424B8396C7}
{DC16CBE1-B1FA-4E6B-AAD8-43CDECA46659} = {7F8AD027-D8D7-407B-9E08-B363B7B34621}
{8F04A78E-D2E1-41AC-B43C-74608B7B4FCB} = {7F8AD027-D8D7-407B-9E08-B363B7B34621}
{332B56C1-8ECB-4E34-9E69-54BF6A192CFB} = {F21CCF39-1F8A-40DA-A0E5-F1426B09BAA2}
{FA8DDDD1-4DD9-4927-A9FB-C4FACDEAF901} = {F21CCF39-1F8A-40DA-A0E5-F1426B09BAA2}
EndGlobalSection
EndGlobal
+27 -27
View File
@@ -42,30 +42,30 @@ dotnet build
## Challenges
| Day | Problem | Solution | Status | Notes |
| --- | -------------------------- | :-----------------------------: | :----: | ------------------------------------------------------------------------------------------------------- |
| 01 | [Problem](./01/PROBLEM.md) | [Solution](./01/Trebuchet/) | ✅ | The trickiest part here was accounting for overlapping digit words. |
| 02 | [Problem](./02/PROBLEM.md) | [Solution](./02/CubeConundrum/) | ✅ | The key to me here was to parse the input into a useful model. |
| 03 | [Problem](./03/PROBLEM.md) | [Solution](./03/GearRatios/) | ✅ | The edge case that got me here was lines ending with a part number. |
| 04 | [Problem](./04/PROBLEM.md) | [Solution](./04/Scratchcards/) | ✅ | Part 2 gets out of hand quickly with just 200 cards. |
| 05 | [Problem](./05/PROBLEM.md) | [Solution](./05/IYGASAF/) | ✅ | I brute forced part 2 using parallelism. I know shame. |
| 06 | [Problem](./06/PROBLEM.md) | [Solution](./06/WaitForIt/) | ✅ | Thank goodness part 2 was not like 5's part 2. 😅 |
| 07 | [Problem](./07/PROBLEM.md) | [Solution](./07/CamelCards/) | ✅ | What took me longest here was I missed a case when jokers are wild and there are three groups of cards. |
| 08 | [Problem](./08/PROBLEM.md) | [Solution](./08/) | |
| 09 | [Problem](./09/PROBLEM.md) | [Solution](./09/) | ⌛ |
| 10 | [Problem](./10/PROBLEM.md) | [Solution](./10/) | ⌛ |
| 11 | [Problem](./11/PROBLEM.md) | [Solution](./11/) | ⌛ |
| 12 | [Problem](./12/PROBLEM.md) | [Solution](./12/) | ⌛ |
| 13 | [Problem](./13/PROBLEM.md) | [Solution](./13/) | ⌛ |
| 14 | [Problem](./14/PROBLEM.md) | [Solution](./14/) | ⌛ |
| 15 | [Problem](./15/PROBLEM.md) | [Solution](./15/) | ⌛ |
| 16 | [Problem](./16/PROBLEM.md) | [Solution](./16/) | ⌛ |
| 17 | [Problem](./17/PROBLEM.md) | [Solution](./17/) | ⌛ |
| 18 | [Problem](./18/PROBLEM.md) | [Solution](./18/) | ⌛ |
| 19 | [Problem](./19/PROBLEM.md) | [Solution](./19/) | ⌛ |
| 20 | [Problem](./20/PROBLEM.md) | [Solution](./20/) | ⌛ |
| 21 | [Problem](./21/PROBLEM.md) | [Solution](./21/) | ⌛ |
| 22 | [Problem](./22/PROBLEM.md) | [Solution](./22/) | ⌛ |
| 23 | [Problem](./23/PROBLEM.md) | [Solution](./23/) | ⌛ |
| 24 | [Problem](./24/PROBLEM.md) | [Solution](./24/) | ⌛ |
| 25 | [Problem](./25/PROBLEM.md) | [Solution](./25/) | ⌛ |
| Day | Problem | Solution | Status | Notes |
| --- | -------------------------- | :--------------------------------: | :----: | ------------------------------------------------------------------------------------------------------- |
| 01 | [Problem](./01/PROBLEM.md) | [Solution](./01/Trebuchet/) | ✅ | The trickiest part here was accounting for overlapping digit words. |
| 02 | [Problem](./02/PROBLEM.md) | [Solution](./02/CubeConundrum/) | ✅ | The key to me here was to parse the input into a useful model. |
| 03 | [Problem](./03/PROBLEM.md) | [Solution](./03/GearRatios/) | ✅ | The edge case that got me here was lines ending with a part number. |
| 04 | [Problem](./04/PROBLEM.md) | [Solution](./04/Scratchcards/) | ✅ | Part 2 gets out of hand quickly with just 200 cards. |
| 05 | [Problem](./05/PROBLEM.md) | [Solution](./05/IYGASAF/) | ✅ | I brute forced part 2 using parallelism. I know shame. |
| 06 | [Problem](./06/PROBLEM.md) | [Solution](./06/WaitForIt/) | ✅ | Thank goodness part 2 was not like 5's part 2. 😅 |
| 07 | [Problem](./07/PROBLEM.md) | [Solution](./07/CamelCards/) | ✅ | What took me longest here was I missed a case when jokers are wild and there are three groups of cards. |
| 08 | [Problem](./08/PROBLEM.md) | [Solution](./08/HauntedWasteland/) | | Got to implement a least common multiple algorithm for part 2. |
| 09 | [Problem](./09/PROBLEM.md) | [Solution](./09/) | ⌛ |
| 10 | [Problem](./10/PROBLEM.md) | [Solution](./10/) | ⌛ |
| 11 | [Problem](./11/PROBLEM.md) | [Solution](./11/) | ⌛ |
| 12 | [Problem](./12/PROBLEM.md) | [Solution](./12/) | ⌛ |
| 13 | [Problem](./13/PROBLEM.md) | [Solution](./13/) | ⌛ |
| 14 | [Problem](./14/PROBLEM.md) | [Solution](./14/) | ⌛ |
| 15 | [Problem](./15/PROBLEM.md) | [Solution](./15/) | ⌛ |
| 16 | [Problem](./16/PROBLEM.md) | [Solution](./16/) | ⌛ |
| 17 | [Problem](./17/PROBLEM.md) | [Solution](./17/) | ⌛ |
| 18 | [Problem](./18/PROBLEM.md) | [Solution](./18/) | ⌛ |
| 19 | [Problem](./19/PROBLEM.md) | [Solution](./19/) | ⌛ |
| 20 | [Problem](./20/PROBLEM.md) | [Solution](./20/) | ⌛ |
| 21 | [Problem](./21/PROBLEM.md) | [Solution](./21/) | ⌛ |
| 22 | [Problem](./22/PROBLEM.md) | [Solution](./22/) | ⌛ |
| 23 | [Problem](./23/PROBLEM.md) | [Solution](./23/) | ⌛ |
| 24 | [Problem](./24/PROBLEM.md) | [Solution](./24/) | ⌛ |
| 25 | [Problem](./25/PROBLEM.md) | [Solution](./25/) | ⌛ |