From 8fb6a19106bff18f78c37cc51cd309f33c4a5c97 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sun, 1 Dec 2024 22:05:41 +0000 Subject: [PATCH 01/43] 2024 day 1 --- AdventOfCode.CSharp/2024/Day1.cs | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day1.cs diff --git a/AdventOfCode.CSharp/2024/Day1.cs b/AdventOfCode.CSharp/2024/Day1.cs new file mode 100644 index 0000000..61b8ba6 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day1.cs @@ -0,0 +1,60 @@ +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day1 : Solver { + public Day1(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private (int[] left, int[] right) Parse() { + var lines = Shared.Split(Input); + var left = new int[lines.Length]; + var right = new int[lines.Length]; + + for (int i = 0, c = lines.Length; i < c; ++i) { + var parts = lines[i].Split(" "); + left[i] = int.Parse(parts[0]); + right[i] = int.Parse(parts[1]); + } + + return (left, right); + } + + protected override long SolvePartOne() { + var (left, right) = Parse(); + + Array.Sort(left); + Array.Sort(right); + + return left.Zip(right, (l, r) => Math.Abs(r - l)).Sum(); + } + + protected override long SolvePartTwo() { + var (left, right) = Parse(); + var rightOccurrences = right.GroupBy(r => r).ToDictionary(g => g.Key, g => g.Count()); + + return left.Where(rightOccurrences.ContainsKey).Sum(l => l * rightOccurrences[l]); + } + + private const string? ExampleInput = @" +3 4 +4 3 +2 5 +1 3 +3 9 +3 3 +"; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day1(ExampleInput, Output).SolvePartOne(); + Assert.Equal(11, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day1(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(31, actual); + } +} \ No newline at end of file From e275ce5d89cb6731884fdf73b47964a99a4ef388 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Mon, 2 Dec 2024 07:01:55 +0000 Subject: [PATCH 02/43] 2024 day 2 --- AdventOfCode.CSharp/2024/Day2.cs | 73 ++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day2.cs diff --git a/AdventOfCode.CSharp/2024/Day2.cs b/AdventOfCode.CSharp/2024/Day2.cs new file mode 100644 index 0000000..8d532ff --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day2.cs @@ -0,0 +1,73 @@ +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day2 : Solver { + public Day2(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private int[][] Parse() { + return Shared.Split(Input) + .Select(s => s.Split(" ").Select(int.Parse).ToArray()) + .ToArray(); + } + + private static bool IsSafe(int[] reports) { + var isIncreasing = reports[1] > reports[0]; + + return reports.Pairwise().All(t => + t.Item1 != t.Item2 && Math.Abs(t.Item2 - t.Item1) < 4 && t.Item2 > t.Item1 == isIncreasing); + } + + private static bool IsSafeWithDampener(int[] reports) { + return + IsSafe(reports) || + Enumerable.Range(0, reports.Length) + .Any(i => IsSafe(reports.Take(i).Concat(reports.Skip(i + 1)).ToArray())); + } + + protected override long SolvePartOne() { + var lines = Parse(); + + return lines.Count(IsSafe); + } + + protected override long SolvePartTwo() { + var lines = Parse(); + + return lines.Count(IsSafeWithDampener); + } + + private const string? ExampleInput = @" +7 6 4 2 1 +1 2 7 8 9 +9 7 6 2 1 +1 3 2 4 5 +8 6 4 4 1 +1 3 6 7 9 +"; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day2(ExampleInput, Output).SolvePartOne(); + Assert.Equal(2, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day2(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(4, actual); + } + + [Theory] + [InlineData(new[] { 7, 6, 4, 2, 1 }, true)] + [InlineData(new[] { 1, 2, 7, 8, 9 }, false)] + [InlineData(new[] { 9, 7, 6, 2, 1 }, false)] + [InlineData(new[] { 1, 3, 2, 4, 5 }, true)] + [InlineData(new[] { 8, 6, 4, 4, 1 }, true)] + [InlineData(new[] { 1, 3, 6, 7, 9 }, true)] + public void SolvesPartTwoLines(int[] line, bool expected) { + Assert.Equal(expected, IsSafeWithDampener(line)); + } +} \ No newline at end of file From 197a3a6f133d7252ce9ac03a0c1a0c0a830758f7 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 3 Dec 2024 07:08:31 +0000 Subject: [PATCH 03/43] 2024 day 3 --- AdventOfCode.CSharp/2024/Day3.cs | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day3.cs diff --git a/AdventOfCode.CSharp/2024/Day3.cs b/AdventOfCode.CSharp/2024/Day3.cs new file mode 100644 index 0000000..7181182 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day3.cs @@ -0,0 +1,60 @@ +using System.Text.RegularExpressions; +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day3 : Solver { + public Day3(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + protected override long SolvePartOne() { + var regex = new Regex(@"mul\(([0-9]{1,3}),([0-9]{1,3})\)"); + return regex.Matches(Input) + .Sum(m => + int.Parse(m.Groups[1].Value) * int.Parse(m.Groups[2].Value)); + } + + protected override long SolvePartTwo() { + var regex = new Regex(@"mul\(([0-9]{1,3}),([0-9]{1,3})\)|don't()|do()"); + var isEnabled = true; + var sum = 0; + + foreach (Match m in regex.Matches(Input)) { + if (m.Value.StartsWith("don't")) { + isEnabled = false; + continue; + } + + if (m.Value.StartsWith("do")) { + isEnabled = true; + continue; + } + + if (isEnabled) + sum += int.Parse(m.Groups[1].Value) * int.Parse(m.Groups[2].Value); + } + + return sum; + } + + private const string? ExampleInput = @" +xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5)) +"; + + private const string? ExamplePart2Input = @" +xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5)) +"; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day3(ExampleInput, Output).SolvePartOne(); + Assert.Equal(161, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day3(ExamplePart2Input, Output).SolvePartTwo(); + Assert.Equal(48, actual); + } +} \ No newline at end of file From d387881a73ec41edc88dd2e55f35e487db5e0178 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Wed, 4 Dec 2024 07:27:01 +0000 Subject: [PATCH 04/43] 2024 day 4 --- AdventOfCode.CSharp/2024/Day4.cs | 116 +++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day4.cs diff --git a/AdventOfCode.CSharp/2024/Day4.cs b/AdventOfCode.CSharp/2024/Day4.cs new file mode 100644 index 0000000..06485d8 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day4.cs @@ -0,0 +1,116 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Grid; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day4 : Solver { + private readonly char[,] grid; + private readonly int width; + private readonly int height; + + public Day4(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { + this.grid = Input.SplitGrid(); + this.width = this.grid.Width(); + this.height = this.grid.Height(); + } + + protected override long SolvePartOne() { + var count = 0; + var word = "XMAS".ToCharArray(); + + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + count += Search(word, x, y, 0, -1); // N Upwards + count += Search(word, x, y, 1, -1); // NE Up-right diagonal + count += Search(word, x, y, 1, 0); // E Forwards + count += Search(word, x, y, 1, 1); // SE Down-right diagonal + count += Search(word, x, y, 0, 1); // S Downwards + count += Search(word, x, y, -1, 1); // SW Down-left diagonal + count += Search(word, x, y, -1, 0); // E Backwards + count += Search(word, x, y, -1, -1); // NW Up-left diagonal + } + } + + return count; + } + + protected override long SolvePartTwo() { + var count = 0; + var word = "MAS".ToCharArray(); + + for (var y = 1; y < height - 1; y++) { + for (var x = 1; x < width - 1; x++) { + // "\" + var isDownRight = 1 == Search(word, x - 1, y - 1, 1, 1); + var isUpLeft = 1 == Search(word, x + 1, y + 1, -1, -1); + + // "/" + var isUpRight = 1 == Search(word, x - 1, y + 1, 1, -1); + var isDownLeft = 1 == Search(word, x + 1, y - 1, -1, 1); + + if ((isDownRight || isUpLeft) && (isUpRight || isDownLeft)) + count += 1; + } + } + + return count; + } + + private int Search(char[] word, int x, int y, int dx, int dy) { + if (grid[y, x] != word[0]) return 0; + + for (int i = 1, c = word.Length; i < c; ++i) { + x += dx; + y += dy; + + if (x < 0 || x >= width || y < 0 || y >= height) + return 0; + + if (grid[y, x] != word[i]) + return 0; + } + + return 1; + } + + private const string? SmallExampleInput = @" +..X... +.SAMX. +.A..A. +XMAS.S +.X.... +"; + + private const string? ExampleInput = @" +MMMSXXMASM +MSAMXMSMSA +AMXSXMAAMM +MSAMASMSMX +XMASAMXAMM +XXAMMXXAMA +SMSMSASXSS +SAXAMASAAA +MAMMMXMMMM +MXMXAXMASX +"; + + [Fact] + public void SolvesSmallExample() { + var actual = new Day4(SmallExampleInput, Output).SolvePartOne(); + Assert.Equal(4, actual); + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day4(ExampleInput, Output).SolvePartOne(); + Assert.Equal(18, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day4(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(9, actual); + } +} \ No newline at end of file From 94182f45c32ec873d9fd6c912ad1b2e1992a85ee Mon Sep 17 00:00:00 2001 From: James Holwell Date: Thu, 5 Dec 2024 06:50:09 +0000 Subject: [PATCH 05/43] 2024 day 5 --- AdventOfCode.CSharp/2024/Day5.cs | 146 +++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day5.cs diff --git a/AdventOfCode.CSharp/2024/Day5.cs b/AdventOfCode.CSharp/2024/Day5.cs new file mode 100644 index 0000000..4210190 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day5.cs @@ -0,0 +1,146 @@ +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day5 : Solver { + public Day5(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private ((int page, int beforePage)[], int[][] updates) Parse(string input) { + var parts = input.SplitBy("\n\n"); + + var rules = parts[0] + .Split() + .Select(s => s.Split("|", 2)).Select(ss => (int.Parse(ss[0]), int.Parse(ss[1]))) + .ToArray(); + + var updates = parts[1] + .Split() + .Select(s => s.Split(',').Select(int.Parse).ToArray()) + .ToArray(); + + return (rules, updates); + } + + private bool ObeysRules(int[] update, (int page, int beforePage)[] rules) { + var positions = update.Select((p, i) => new KeyValuePair(p, i)) + .ToDictionary(p => p.Key, p => p.Value); + + foreach (var rule in rules) { + if (!positions.TryGetValue(rule.page, out var pageIndex) || + !positions.TryGetValue(rule.beforePage, out var beforePageIndex)) + continue; + + if (pageIndex > beforePageIndex) + return false; + } + + return true; + } + + private static int[] FixUpdate(int[] update, (int page, int beforePage)[] rules) { + var fixedUpdate = new List(update); + + var positions = fixedUpdate.Select((p, i) => new KeyValuePair(p, i)) + .ToDictionary(p => p.Key, p => p.Value); + + var isUnfinished = true; + while (isUnfinished) { + isUnfinished = false; + + foreach (var rule in rules) { + if (!positions.TryGetValue(rule.page, out var pageIndex) || + !positions.TryGetValue(rule.beforePage, out var beforePageIndex)) + continue; + + if (pageIndex <= beforePageIndex) + continue; + + + var movingPage = fixedUpdate[pageIndex]; + fixedUpdate.RemoveAt(pageIndex); + fixedUpdate.Insert(beforePageIndex, movingPage); + + positions = fixedUpdate.Select((p, i) => new KeyValuePair(p, i)) + .ToDictionary(p => p.Key, p => p.Value); + + isUnfinished = true; + break; + } + } + + return fixedUpdate.ToArray(); + } + + protected override long SolvePartOne() { + var (rules, updates) = Parse(Input); + + return updates + .Where(update => ObeysRules(update, rules)) + .Sum(update => update[update.Length / 2]); + } + + protected override long SolvePartTwo() { + var (rules, updates) = Parse(Input); + + return updates + .Where(update => !ObeysRules(update, rules)) + .Select(update => FixUpdate(update, rules)) + .Sum(update => update[update.Length / 2]); + } + + private const string? ExampleInput = @" +47|53 +97|13 +97|61 +97|47 +75|29 +61|13 +75|53 +29|13 +97|29 +53|29 +61|53 +97|53 +61|29 +47|13 +75|47 +97|75 +47|61 +75|61 +47|29 +75|13 +53|13 + +75,47,61,53,29 +97,61,53,29,13 +75,29,13 +75,97,47,61,53 +61,13,29 +97,13,75,29,47 +"; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day5(ExampleInput, Output).SolvePartOne(); + Assert.Equal(143, actual); + } + + + [Theory] + [InlineData(new[] { 75, 97, 47, 61, 53 }, new[] { 97, 75, 47, 61, 53 })] + [InlineData(new[] { 61, 13, 29 }, new[] { 61, 29, 13 })] + [InlineData(new[] { 97, 13, 75, 29, 47 }, new[] { 97, 75, 47, 29, 13 })] + public void FixReordersUpdatesCorrectly(int[] update, int[] expected) { + var (rules, _) = Parse(ExampleInput!); + + Assert.Equal(expected, FixUpdate(update, rules)); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day5(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(123, actual); + } +} \ No newline at end of file From 8166783575ef253061cee5216b9d9b2f9629d1ad Mon Sep 17 00:00:00 2001 From: James Holwell Date: Fri, 6 Dec 2024 07:02:52 +0000 Subject: [PATCH 06/43] 2024 day 6 --- AdventOfCode.CSharp/2024/Day6.cs | 186 +++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day6.cs diff --git a/AdventOfCode.CSharp/2024/Day6.cs b/AdventOfCode.CSharp/2024/Day6.cs new file mode 100644 index 0000000..6b45908 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day6.cs @@ -0,0 +1,186 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Grid; +using AdventOfCode.Core.Output; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day6 : Solver { + public Day6(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private static (int x, int y) GetStartPosition(int height, int width, char[,] grid) { + for (var y = 0; y < height; y++) + for (var x = 0; x < width; x++) + if (grid[y, x] == '^') + return (x, y); + + throw new InvalidOperationException(); + } + + private static (int x, int y) NextPosition(int cx, int cy, Directions direction) { + return direction switch { + Directions.North => (cx, cy - 1), + Directions.East => (cx + 1, cy), + Directions.South => (cx, cy + 1), + Directions.West => (cx - 1, cy), + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + } + + private static Directions RightTurnFor(Directions direction) { + return direction switch { + Directions.North => Directions.East, + Directions.East => Directions.South, + Directions.South => Directions.West, + Directions.West => Directions.North, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + } + + protected override long SolvePartOne() { + var grid = Input.SplitGrid(); + var width = grid.Width(); + var height = grid.Height(); + + var (cx, cy) = GetStartPosition(height, width, grid); + var direction = Directions.North; + + while (true) { + grid[cy, cx] = 'X'; + + var (nx, ny) = NextPosition(cx, cy, direction); + + if (0 <= nx && nx < width && 0 <= ny && ny < height) { + if (grid[ny, nx] != '#') { + cx = nx; + cy = ny; + continue; + } + + direction = RightTurnFor(direction); + continue; + } + + break; + } + + Trace.WriteLine(grid.Render()); + return grid.Count(c => c == 'X'); + } + + protected override long SolvePartTwo() { + var originalGrid = Input.SplitGrid(); + var width = originalGrid.Width(); + var height = originalGrid.Height(); + + var (ox, oy) = GetStartPosition(height, width, originalGrid); + var sum = 0; + + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + if (originalGrid[y, x] != '.') + continue; + + var foundLoop = false; + + // initialize new grid for this obstacle + var grid = new char[height, width]; + Array.Copy(originalGrid, grid, originalGrid.Length); + grid[y, x] = 'O'; + + // initialize where deja-vu grid + var dejaGrid = new Directions[height, width]; + dejaGrid.Initialize(Directions.None); + + // set up start position + int cx = ox, cy = oy; + var direction = Directions.North; + + // walk the newGrid to see if we find a loop + while (true) { + if (dejaGrid[cy, cx].HasFlag(direction)) { + foundLoop = true; + break; + } + + dejaGrid[cy, cx] |= direction; + + var (nx, ny) = NextPosition(cx, cy, direction); + + if (0 <= nx && nx < width && 0 <= ny && ny < height) { + if (grid[ny, nx] == '#' || grid[ny, nx] == 'O') { + grid[cy, cx] = '+'; + + direction = RightTurnFor(direction); + continue; + } + + if (grid[ny, nx] == '.') { + grid[ny, nx] = direction switch { + Directions.North or Directions.South => '|', + Directions.East or Directions.West => '-', + _ => throw new ArgumentOutOfRangeException() + }; + + cx = nx; + cy = ny; + continue; + } + + if (grid[ny, nx] == '|' || grid[ny, nx] == '-') { + grid[ny, nx] = '+'; + } + + cx = nx; + cy = ny; + continue; + } + + break; + } + + if (foundLoop) { + sum++; + Trace.WriteLine(grid.Render()); + Trace.WriteLine(); + } + } + } + + return sum; + } + [Flags] + private enum Directions { + None = 0, + North = 1, + East = 2, + South = 4, + West = 8 + } + + private const string? ExampleInput = @" +....#..... +.........# +.......... +..#....... +.......#.. +.......... +.#..^..... +........#. +#......... +......#... +"; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day6(ExampleInput, Output).SolvePartOne(); + Assert.Equal(41, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day6(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(6, actual); + } +} \ No newline at end of file From fbd3356fe66bcf9bacc687cf5303c30482bc4e4b Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sat, 7 Dec 2024 12:06:35 +0000 Subject: [PATCH 07/43] 2024 day 7 --- AdventOfCode.CSharp/2024/Day7.cs | 154 +++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day7.cs diff --git a/AdventOfCode.CSharp/2024/Day7.cs b/AdventOfCode.CSharp/2024/Day7.cs new file mode 100644 index 0000000..8823cea --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day7.cs @@ -0,0 +1,154 @@ +using System.Linq.Expressions; +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day7 : Solver { + public Day7(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private static (long Value, long[] Operands)[] Parse(string input) { + return Shared.Split(input) + .Select(s => { + var parts = s.Split(new[] { ':', ' ' }, + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + return (long.Parse(parts[0]), parts.Skip(1).Select(long.Parse).ToArray()); + }) + .ToArray(); + } + + private readonly Dictionary[]> evaluatorsCache = new(); + + private Func[] GetEvaluators(int length, params Operators[] availableOperators) { + if (evaluatorsCache.TryGetValue(length, out var cachedEvaluators)) + return cachedEvaluators; + + var operandsParameter = Expression.Parameter(typeof(long[])); + + var operators = availableOperators.Select(ao => new[] { ao }).ToArray(); + var combinations = operators.ToArray(); + for (var i = 1; i < length - 1; i++) { + combinations = combinations + .SelectMany(c => operators.Select(o => c.Concat(o).ToArray())) + .ToArray(); + } + + var evaluators = new Func[combinations.Length]; + var evaluatorIndex = 0; + + var floor = typeof(Math).GetMethod(nameof(Math.Floor), new[] { typeof(double) })!; + var pow = typeof(Math).GetMethod(nameof(Math.Pow))!; + var log10 = typeof(Math).GetMethod(nameof(Math.Log10))!; + + foreach (var combination in combinations) { + var parameterIdx = 0; + Expression expression = Expression.ArrayAccess(operandsParameter, Expression.Constant(parameterIdx++)); + + expression = combination.Aggregate(expression, (current, operation) => operation switch { + Operators.Add => Expression.Add(current, Expression.ArrayAccess(operandsParameter, Expression.Constant(parameterIdx++))), + + Operators.Mul => Expression.Multiply(current, Expression.ArrayAccess(operandsParameter, Expression.Constant(parameterIdx++))), + + Operators.Concat => + Expression.Add( + Expression.Multiply( + current, + Expression.Convert( + Expression.Call( + pow, + Expression.Constant(10D), + Expression.Add( + Expression.Constant(1D), + Expression.Call( + floor, + Expression.Call( + log10, + Expression.Convert(Expression.ArrayAccess(operandsParameter, Expression.Constant(parameterIdx)), typeof(double)))))), + typeof(long))), + Expression.ArrayAccess(operandsParameter, Expression.Constant(parameterIdx++))), + _ => throw new ArgumentOutOfRangeException() + }); + + var evaluator = Expression.Lambda>(expression, operandsParameter); + Trace.WriteLine(evaluator.ToString()); + + evaluators[evaluatorIndex++] = evaluator.Compile(); + } + + evaluatorsCache.Add(length, evaluators); + return evaluators; + } + + protected override long SolvePartOne() { + var equations = Parse(Input); + + var sum = 0L; + foreach (var (value, operands) in equations) { + var evaluators = GetEvaluators(operands.Length, Operators.Add, Operators.Mul); + + if (evaluators.Any(e => value == e(operands))) + sum += value; + } + + return sum; + } + + protected override long SolvePartTwo() { + var equations = Parse(Input); + + var sum = 0L; + foreach (var (value, operands) in equations) { + var evaluators = GetEvaluators(operands.Length, Operators.Add, Operators.Mul, Operators.Concat); + + if (evaluators.Any(e => value == e(operands))) + sum += value; + } + + return sum; + } + + private enum Operators { + Add, Mul, Concat + } + + private const string? ExampleInput = @" +190: 10 19 +3267: 81 40 27 +83: 17 5 +156: 15 6 +7290: 6 8 6 15 +161011: 16 10 13 +192: 17 8 14 +21037: 9 7 18 13 +292: 11 6 16 20 +"; + + [Theory] + [InlineData(1, 190, new long[] { 10, 19 })] + [InlineData(2, 3267, new long[] { 81, 40, 27 })] + [InlineData(0, 83, new long[] { 17, 5 })] + [InlineData(0, 156, new long[] { 15, 6 })] + [InlineData(0, 7290, new long[] { 6, 8, 6, 15 })] + [InlineData(0, 161011, new long[] { 16, 10, 13 })] + [InlineData(0, 192, new long[] { 17, 8, 14 })] + [InlineData(0, 21037, new long[] { 9, 7, 18, 13 })] + [InlineData(1, 292, new long[] { 11, 6, 16, 20 })] + public void SolvesIndividualPartOneLines(int expectedSolutions, long value, long[] operands) { + var evaluators = GetEvaluators(operands.Length, Operators.Add, Operators.Mul); + var actual = evaluators.Count(e => value == e(operands)); + Assert.Equal(expectedSolutions, actual); + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day7(ExampleInput, Output).SolvePartOne(); + Assert.Equal(3749, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day7(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(11387, actual); + } +} \ No newline at end of file From d04d185bba16ab85d9bc47b49ec846b91aadc2d4 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Mon, 9 Dec 2024 07:18:23 +0000 Subject: [PATCH 08/43] 2024 day 8 --- AdventOfCode.CSharp/2024/Day8.cs | 164 +++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day8.cs diff --git a/AdventOfCode.CSharp/2024/Day8.cs b/AdventOfCode.CSharp/2024/Day8.cs new file mode 100644 index 0000000..b33dcaf --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day8.cs @@ -0,0 +1,164 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Grid; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day8 : Solver { + public Day8(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + protected override long SolvePartOne() { + var grid = Input.SplitGrid(); + var height = grid.Height(); + var width = grid.Width(); + + var outputGrid = grid.Duplicate(); + var antiNodes = new char[height, width]; + antiNodes.Initialize('.'); + + foreach (var antennas in GetAntennaGroups(height, width, grid)) { + foreach (var (left, right) in antennas.Value.Combinations()) { + var vector = (dx: right.x - left.x, dy: right.y - left.y); + + var antiNode1 = (x: left.x - vector.dx, y: left.y - vector.dy); + if (0 <= antiNode1.x && antiNode1.x < width && 0 <= antiNode1.y && antiNode1.y < height) { + antiNodes[antiNode1.y, antiNode1.x] = '#'; + if (outputGrid[antiNode1.y, antiNode1.x] == '.') + outputGrid[antiNode1.y, antiNode1.x] = '#'; + } + + var antiNode2 = (x: right.x + vector.dx, y: right.y + vector.dy); + if (0 <= antiNode2.x && antiNode2.x < width && 0 <= antiNode2.y && antiNode2.y < height) { + antiNodes[antiNode2.y, antiNode2.x] = '#'; + + if (outputGrid[antiNode2.y, antiNode2.x] == '.') + outputGrid[antiNode2.y, antiNode2.x] = '#'; + } + } + } + + Trace.WriteLine(outputGrid.Render()); + return antiNodes.Count(c => c == '#'); + } + + protected override long SolvePartTwo() { + var grid = Input.SplitGrid(); + var height = grid.Height(); + var width = grid.Width(); + + var outputGrid = grid.Duplicate(); + var antiNodes = new char[height, width]; + antiNodes.Initialize('.'); + + foreach (var antennas in GetAntennaGroups(height, width, grid)) { + foreach (var (left, right) in antennas.Value.Combinations()) { + var vector = (dx: right.x - left.x, dy: right.y - left.y); + + var antiNode1 = left; + while (0 <= antiNode1.x && antiNode1.x < width && 0 <= antiNode1.y && antiNode1.y < height) { + antiNodes[antiNode1.y, antiNode1.x] = '#'; + if (outputGrid[antiNode1.y, antiNode1.x] == '.') + outputGrid[antiNode1.y, antiNode1.x] = '#'; + + antiNode1 = (x: antiNode1.x - vector.dx, y: antiNode1.y - vector.dy); + } + + var antiNode2 = (x: left.x + vector.dx, y: left.y + vector.dy); + while (0 <= antiNode2.x && antiNode2.x < width && 0 <= antiNode2.y && antiNode2.y < height) { + antiNodes[antiNode2.y, antiNode2.x] = '#'; + + if (outputGrid[antiNode2.y, antiNode2.x] == '.') + outputGrid[antiNode2.y, antiNode2.x] = '#'; + + antiNode2 = (x: antiNode2.x + vector.dx, y: antiNode2.y + vector.dy); + } + } + } + + Trace.WriteLine(outputGrid.Render()); + return antiNodes.Count(c => c == '#'); + } + + private static Dictionary> GetAntennaGroups(int height, int width, char[,] grid) { + var antennaGroups = new Dictionary>(); + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + if (grid[y, x] == '.') + continue; + + if (!antennaGroups.ContainsKey(grid[y, x])) + antennaGroups[grid[y, x]] = new List<(int x, int y)>() { (x, y) }; + else + antennaGroups[grid[y, x]].Add((x, y)); + } + } + + return antennaGroups; + } + + [Fact] + public void SolvesSimplePartOneExample() { + const string simpleInput = @" +.......... +.......... +.......... +....a..... +.......... +.....a.... +.......... +.......... +.......... +.......... +"; + + var actual = new Day8(simpleInput, Output).SolvePartOne(); + Assert.Equal(2, actual); + } + + [Fact] + public void SolvesSimplePartTwoExample() { + const string simpleInput = @" +T......... +...T...... +.T........ +.......... +.......... +.......... +.......... +.......... +.......... +.......... +"; + + var actual = new Day8(simpleInput, Output).SolvePartTwo(); + Assert.Equal(9, actual); + } + + private const string? ExampleInput = @" +............ +........0... +.....0...... +.......0.... +....0....... +......A..... +............ +............ +........A... +.........A.. +............ +............ +"; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day8(ExampleInput, Output).SolvePartOne(); + Assert.Equal(14, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day8(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(34, actual); + } +} \ No newline at end of file From ba706431bc41bc255f9d771a7226045731508498 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Mon, 9 Dec 2024 07:27:27 +0000 Subject: [PATCH 09/43] 2024 day 9 --- AdventOfCode.CSharp/2024/Day9.cs | 134 +++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day9.cs diff --git a/AdventOfCode.CSharp/2024/Day9.cs b/AdventOfCode.CSharp/2024/Day9.cs new file mode 100644 index 0000000..35ae984 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day9.cs @@ -0,0 +1,134 @@ +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day9 : Solver { + public Day9(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private static int?[] BuildDisk(string? input) { + var chars = input!.Trim().ToCharArray().Select(c => c - '0').ToArray(); + var length = chars.Sum(c => c); + var disk = new int?[length]; + var pos = 0; + var id = 0; + + for (var i = 0; i < chars.Length; ++i) { + for (int j = 0, c = chars[i]; j < c; ++j) + disk[pos++] = id; + + if (++i < chars.Length) + pos += chars[i]; + + ++id; + } + + return disk; + } + + private static long Checksum(int?[] disk) { + return disk.Select((value, index) => value.HasValue ? (long)value.Value * index : 0L).Sum(); + } + + protected override long SolvePartOne() { + var disk = BuildDisk(Input); + + var dataPointer = disk.Length - 1; + var freeSpacePointer = 0; + while (freeSpacePointer < dataPointer) { + if (!disk[dataPointer].HasValue) { + dataPointer--; + continue; + } + + while (disk[freeSpacePointer].HasValue) + freeSpacePointer++; + + if (freeSpacePointer >= dataPointer) + break; + + disk[freeSpacePointer] = disk[dataPointer]; + disk[dataPointer] = null; + dataPointer--; + } + + return Checksum(disk); + } + + protected override long SolvePartTwo() { + var disk = BuildDisk(Input); + + var dataPointer = (start: disk.Length - 1, end: -1); + var seenIds = new HashSet(); + + while (0 < dataPointer.start) { + // seek to the next extent + while (dataPointer.start > 0 && !disk[dataPointer.start].HasValue) + dataPointer.start--; + + // don't run off the end + if (dataPointer.start == 0) + break; + + // don't try to move the same file twice + var fileId = disk[dataPointer.start]!.Value; + if (!seenIds.Add(fileId)) { + dataPointer.start--; + continue; + } + + // seek to the beginning of the extent + dataPointer.end = dataPointer.start; + while (dataPointer.start > 0 && disk[dataPointer.start - 1] == fileId) + dataPointer.start--; + + // search for free space + var requiredSpace = dataPointer.end - dataPointer.start; + var freeSpacePointer = 0; + for (var i = 0; i < dataPointer.start; ++i) { + if (disk[i].HasValue) { + freeSpacePointer = i + 1; + continue; + } + + if (i - freeSpacePointer < requiredSpace) + continue; + + for (var j = 0; j <= requiredSpace; ++j) { + disk[freeSpacePointer + j] = disk[dataPointer.start + j]; + disk[dataPointer.start + j] = null; + } + + break; + } + + // reset the extent + dataPointer.start--; + } + + return Checksum(disk); + } + + private const string? ExampleInput = @" +2333133121414131402 +"; + + [Fact] + public void SolvesSimpleExample() { + var actual = new Day9("12345", Output).SolvePartOne(); + Assert.Equal(60, actual); + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day9(ExampleInput, Output).SolvePartOne(); + Assert.Equal(1928, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day9(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(2858, actual); + } +} \ No newline at end of file From 9e5c5c4c6bfb56afca17ef39ca55e618bca2eb42 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 10 Dec 2024 07:56:15 +0000 Subject: [PATCH 10/43] 2024 day 10 --- AdventOfCode.CSharp/2024/Day10.cs | 191 ++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day10.cs diff --git a/AdventOfCode.CSharp/2024/Day10.cs b/AdventOfCode.CSharp/2024/Day10.cs new file mode 100644 index 0000000..25ac64f --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day10.cs @@ -0,0 +1,191 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Grid; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day10 : Solver { + public Day10(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private static int WalkMap(int height, int width, int[,] grid, T current, T next) where T : ICollection<(int ox, int oy, int x, int y)>, new() { + for (var y = 0; y < height; y++) + for (var x = 0; x < width; x++) + if (grid[y, x] == 9) + current.Add((x, y, x, y)); + + for (var i = 8; i >= 0; --i) { + foreach (var pos in current) { + if (pos.y > 0 && grid[pos.y - 1, pos.x] == i) + next.Add(pos with { y = pos.y - 1 }); + + if (pos.x < width - 1 && grid[pos.y, pos.x + 1] == i) + next.Add(pos with { x = pos.x + 1 }); + + if (pos.y < height - 1 && grid[pos.y + 1, pos.x] == i) + next.Add(pos with { y = pos.y + 1 }); + + if (pos.x > 0 && grid[pos.y, pos.x - 1] == i) + next.Add(pos with { x = pos.x - 1 }); + } + + current = [..next]; + next.Clear(); + } + + return current.Count; + } + + protected override long SolvePartOne() { + var grid = Input.SplitGrid(c => c - '0'); + var height = grid.Height(); + var width = grid.Width(); + + var current = new HashSet<(int ox, int oy, int x, int y)>(); + var next = new HashSet<(int ox, int oy, int x, int y)>(); + return WalkMap(height, width, grid, current, next); + } + + protected override long SolvePartTwo() { + var grid = Input.SplitGrid(c => c - '0'); + var height = grid.Height(); + var width = grid.Width(); + + var current = new List<(int ox, int oy, int x, int y)>(); + var next = new List<(int ox, int oy, int x, int y)>(); + return WalkMap(height, width, grid, current, next); + } + + [Fact] + public void SolveSimplePartOneExample1() { + const string simpleInput = @" +0123 +1234 +8765 +9876 +"; + + var actual = new Day10(simpleInput, Output).SolvePartOne(); + Assert.Equal(1, actual); + } + + [Fact] + public void SolveSimplePartOneExample2() { + const string simpleInput = @" +...0... +...1... +...2... +6543456 +7.....7 +8.....8 +9.....9 +"; + + var actual = new Day10(simpleInput, Output).SolvePartOne(); + Assert.Equal(2, actual); + } + + [Fact] + public void SolveSimplePartOneExample3() { + const string simpleInput = @" +..90..9 +...1.98 +...2..7 +6543456 +765.987 +876.... +987.... +"; + + var actual = new Day10(simpleInput, Output).SolvePartOne(); + Assert.Equal(4, actual); + } + + [Fact] + public void SolveSimplePartOneExample4() { + const string simpleInput = @" +10..9.. +2...8.. +3...7.. +4567654 +...8..3 +...9..2 +.....01 +"; + + var actual = new Day10(simpleInput, Output).SolvePartOne(); + Assert.Equal(3, actual); + } + + + private const string? ExampleInput = @" +89010123 +78121874 +87430965 +96549874 +45678903 +32019012 +01329801 +10456732 +"; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day10(ExampleInput, Output).SolvePartOne(); + Assert.Equal(36, actual); + } + + + [Fact] + public void SolveSimplePartTwoExample1() { + const string simpleInput = @" +.....0. +..4321. +..5..2. +..6543. +..7..4. +..8765. +..9.... +"; + + var actual = new Day10(simpleInput, Output).SolvePartTwo(); + Assert.Equal(3, actual); + } + + [Fact] + public void SolveSimplePartTwoExample2() { + const string simpleInput = @" +..90..9 +...1.98 +...2..7 +6543456 +765.987 +876.... +987.... +"; + + var actual = new Day10(simpleInput, Output).SolvePartTwo(); + Assert.Equal(13, actual); + } + + [Fact] + public void SolveSimplePartTwoExample3() { + const string simpleInput = @" +012345 +123456 +234567 +345678 +4.6789 +56789. +"; + + var actual = new Day10(simpleInput, Output).SolvePartTwo(); + Assert.Equal(227, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day10(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(81, actual); + } +} \ No newline at end of file From 85d7f8c84c45415a2d795e94d113eeacb6966d09 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Wed, 11 Dec 2024 13:58:13 +0000 Subject: [PATCH 11/43] 2024 day 11 --- AdventOfCode.CSharp/2024/Day11.cs | 187 ++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day11.cs diff --git a/AdventOfCode.CSharp/2024/Day11.cs b/AdventOfCode.CSharp/2024/Day11.cs new file mode 100644 index 0000000..82f5c6b --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day11.cs @@ -0,0 +1,187 @@ +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day11 : Solver { + public Day11(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private static List Blink(ICollection stones) { + var nextStones = new List((int) Math.Ceiling(stones.Count * 1.25)); + + foreach (var stone in stones) { + switch (stone) { + case 0: + nextStones.Add(1); + continue; + + // 12 -> 1, 2 + case >= 10 and < 100: { + var l = stone / 10; + var r = stone - 10 * l; + nextStones.Add(l); + nextStones.Add(r); + continue; + } + + // 1234 -> 12, 34 + case >= 10_00 and < 100_00: { + var l = stone / 100; + var r = stone - 100 * l; + nextStones.Add(l); + nextStones.Add(r); + continue; + } + + // 123456 -> 123, 456 + case >= 10_00_00 and < 100_00_00: { + var l = stone / 1000; + var r = stone - 1000 * l; + nextStones.Add(l); + nextStones.Add(r); + continue; + } + + // 12345678 -> 1234, 5678 + case >= 10_00_00_00 and < 100_00_00_00: { + var l = stone / 10000; + var r = stone - 10000 * l; + nextStones.Add(l); + nextStones.Add(r); + continue; + } + + // 1234567890 -> 12345, 67890 + case >= 10_00_00_00_00 and < 100_00_00_00_00: { + var l = stone / 100000; + var r = stone - 100000 * l; + nextStones.Add(l); + nextStones.Add(r); + continue; + } + + // 123456789012 -> 123456, 789012 + case >= 10_00_00_00_00_00 and < 100_00_00_00_00_00: { + var l = stone / 1000000; + var r = stone - 1000000 * l; + nextStones.Add(l); + nextStones.Add(r); + continue; + } + + // 12345678901234 -> 1234567, 8901234 + case >= 10_00_00_00_00_00_00 and < 100_00_00_00_00_00_00: { + var l = stone / 10000000; + var r = stone - 10000000 * l; + nextStones.Add(l); + nextStones.Add(r); + continue; + } + + case >= 100_00_00_00_00_00: + throw new NotImplementedException(); + + default: + nextStones.Add(stone * 2024); + break; + } + } + + return nextStones; + } + + protected override long SolvePartOne() { + var stones = Input.SplitInt(" ").Select(i => (long)i).ToList(); + + for (var i = 0; i < 25; ++i) { + var nextStones = Blink(stones); + + stones = nextStones; + Trace.WriteLine(string.Join(" ", stones)); + } + + return stones.Count; + } + + protected override long SolvePartTwo() => Solve(Input, 75); + + private static long Solve(string input, int steps) { + return input.SplitInt(" ") + .Select(i => (engraving: (long) i, remaining: steps)) + .Select(CachedSolveInner) + .Sum(); + } + + private static readonly Dictionary<(long, int), long> SolutionCache = new(); + + private static long CachedSolveInner((long engraving, int remaining) stone) { + if (SolutionCache.TryGetValue(stone, out var cachedAnswer)) + return cachedAnswer; + + return SolutionCache[stone] = SolveInner(stone); + } + + private static long SolveInner((long engraving, int remaining) stone) { + if (stone.remaining == 0) + return 1; + + // first rule matches + if (stone.engraving == 0) + return CachedSolveInner((1, stone.remaining - 1)); + + // second rule *doesn't* match + var digits = 1 + (int)Math.Log10(stone.engraving); + if (digits % 2 != 0) + return CachedSolveInner((2024 * stone.engraving, stone.remaining - 1)); + + // split into two halves + var exp = (int) Math.Pow(10, digits >> 1); + var r = stone.engraving / exp; + var l = stone.engraving - exp * r; + return CachedSolveInner((r, stone.remaining - 1)) + + CachedSolveInner((l, stone.remaining - 1)); + } + + [Fact] + public void BlinkReturnsExpectedResults() { + var simpleExample = new long[] {0, 1, 10, 99, 999}; + var actual = Blink(simpleExample); + + var expected = new long[] {1, 2024, 1, 0, 9, 9, 2021976}; + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(125, 1)] + [InlineData(17, 2)] + public void SolveInnerReturnsExpectedResults(int engraving, int expected) { + var actual = SolveInner((engraving, 1)); + Assert.Equal(expected, actual); + } + + [Fact] + public void SolvesSimplePartOneExampleUsingFastAlgorithm() { + const string simpleExample = "0 1 10 99 999"; + var actual = Solve(simpleExample, 1); + Assert.Equal(7, actual); + } + + private const string? ExampleInput = @" +125 17 +"; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day11(ExampleInput, Output).SolvePartOne(); + Assert.Equal(55312, actual); + } + + + [Fact] + public void SolvesPartOneExampleUsingFastAlgorithm() { + var actual = Solve(ExampleInput!, 25); + Assert.Equal(55312, actual); + } +} \ No newline at end of file From c114febd48e681ab1887bdea55572b2d8680d0c6 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Thu, 12 Dec 2024 20:17:24 +0000 Subject: [PATCH 12/43] 2024 day 12 --- AdventOfCode.CSharp/2024/Day12.cs | 192 ++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day12.cs diff --git a/AdventOfCode.CSharp/2024/Day12.cs b/AdventOfCode.CSharp/2024/Day12.cs new file mode 100644 index 0000000..b9c8d6e --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day12.cs @@ -0,0 +1,192 @@ +// ReSharper disable StringLiteralTypo + +using AdventOfCode.Core; +using AdventOfCode.Core.Grid; +using AdventOfCode.Core.Points; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day12 : Solver { + public Day12(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private List> ExtractRegions() { + var grid = Input.SplitGrid(); + var regions = new List>(); + + var unexplored = Input.SplitCoordinates(_ => true).ToHashSet(); + while (unexplored.Count > 0) { + var unexploredCoordinate = unexplored.First(); + var plant = grid.At(unexploredCoordinate); + + var region = new HashSet<(int, int)>(); + var next = new Queue<(int, int)>(); + next.Enqueue(unexploredCoordinate); + + while (next.TryDequeue(out var coordinate)) { + if (!region.Add(coordinate)) continue; + unexplored.Remove(coordinate); + + if (grid.MaybeAt(coordinate.Up()) == plant) + next.Enqueue(coordinate.Up()); + + if (grid.MaybeAt(coordinate.Right()) == plant) + next.Enqueue(coordinate.Right()); + + if (grid.MaybeAt(coordinate.Down()) == plant) + next.Enqueue(coordinate.Down()); + + if (grid.MaybeAt(coordinate.Left()) == plant) + next.Enqueue(coordinate.Left()); + } + + regions.Add(region); + } + + return regions; + } + + private static int PerimeterOf(HashSet<(int x, int y)> region) => + region.Sum(r => + (region.Contains(r.Up()) ? 0 : 1) + + (region.Contains(r.Right()) ? 0 : 1) + + (region.Contains(r.Down()) ? 0 : 1) + + (region.Contains(r.Left()) ? 0 : 1) + ); + + private static int NumberOfSides(HashSet<(int x, int y)> region) { + var upFences = new HashSet<(int x, int y)>(); + var rightFences = new HashSet<(int x, int y)>(); + var downFences = new HashSet<(int x, int y)>(); + var leftFences = new HashSet<(int x, int y)>(); + + // figure out the fence locations + foreach (var plot in region) { + if (!region.Contains(plot.Up())) + upFences.Add(plot); + + if (!region.Contains(plot.Right())) + rightFences.Add(plot); + + if (!region.Contains(plot.Down())) + downFences.Add(plot); + + if (!region.Contains(plot.Left())) + leftFences.Add(plot); + } + + // figure out the runs + var upRuns = upFences.Count(f => upFences.Contains(f.Right())); + var rightRuns = rightFences.Count(f => rightFences.Contains(f.Down())); + var downRuns = downFences.Count(f => downFences.Contains(f.Left())); + var leftRuns = leftFences.Count(f => leftFences.Contains(f.Up())); + + return upFences.Count + rightFences.Count + downFences.Count + leftFences.Count + - upRuns - rightRuns - downRuns - leftRuns; + } + + protected override long SolvePartOne() => ExtractRegions().Sum(region => region.Count * PerimeterOf(region)); + + protected override long SolvePartTwo() => ExtractRegions().Sum(region => region.Count * NumberOfSides(region)); + + [Fact] + public void SolvesSimplePartOneExample() { + const string simpleExample = + """ + AAAA + BBCD + BBCC + EEEC + """; + + var actual = new Day12(simpleExample, Output).SolvePartOne(); + Assert.Equal(4 * 10 + 4 * 8 + 4 * 10 + 3 * 8 + 1 * 4, actual); + } + + [Fact] + public void SolvesSimplePartTwoExample1() { + const string simpleExample = + """ + AAAA + BBCD + BBCC + EEEC + """; + + var actual = new Day12(simpleExample, Output).SolvePartTwo(); + Assert.Equal(80, actual); + } + + [Fact] + public void SolvesSimplePartTwoExample2() { + const string simpleExample = + """ + OOOOO + OXOXO + OOOOO + OXOXO + OOOOO + """; + + var actual = new Day12(simpleExample, Output).SolvePartTwo(); + Assert.Equal(436, actual); + } + + [Fact] + public void SolvesSimplePartTwoExample3() { + const string simpleExample = + """ + EEEEE + EXXXX + EEEEE + EXXXX + EEEEE + """; + + var actual = new Day12(simpleExample, Output).SolvePartTwo(); + Assert.Equal(236, actual); + } + + [Fact] + public void SolvesSimplePartTwoExample4() { + const string simpleExample = + """ + AAAAAA + AAABBA + AAABBA + ABBAAA + ABBAAA + AAAAAA + """; + + var actual = new Day12(simpleExample, Output).SolvePartTwo(); + Assert.Equal(368, actual); + } + + private const string? ExampleInput = + """ + RRRRIICCFF + RRRRIICCCF + VVRRRCCFFF + VVRCCCJFFF + VVVVCJJCFE + VVIVCCJJEE + VVIIICJJEE + MIIIIIJJEE + MIIISIJEEE + MMMISSJEEE + """; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day12(ExampleInput, Output).SolvePartOne(); + Assert.Equal(1930, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day12(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(1206, actual); + } +} \ No newline at end of file From d53403e798d4af540ddaf93fcf7cc925336b86af Mon Sep 17 00:00:00 2001 From: James Holwell Date: Fri, 13 Dec 2024 11:45:45 +0000 Subject: [PATCH 13/43] 2024 day 13 --- AdventOfCode.CSharp/2024/Day13.cs | 192 ++++++++++++++++++ .../AdventOfCode.CSharp.csproj | 1 + 2 files changed, 193 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day13.cs diff --git a/AdventOfCode.CSharp/2024/Day13.cs b/AdventOfCode.CSharp/2024/Day13.cs new file mode 100644 index 0000000..1a6ba11 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day13.cs @@ -0,0 +1,192 @@ +using AdventOfCode.Core; +using Microsoft.Z3; +using Xunit; +using Xunit.Abstractions; +using Solver = AdventOfCode.Core.Solver; + +namespace AdventOfCode.CSharp._2024; + +public class Day13 : Solver { + public Day13(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private static IEnumerable<(int ax, int ay, int bx, int by, long px, long py)> Parse(string s) { + var blocks = s.SplitBy("\n\n").Select(Shared.Split); + foreach (var block in blocks) { + var a = block[0]["Button A: ".Length..].Split(", "); + var b = block[1]["Button B: ".Length..].Split(", "); + var p = block[2]["Prize: ".Length..].Split(", "); + yield return ( + int.Parse(a[0]["X+".Length..]), + int.Parse(a[1]["Y+".Length..]), + int.Parse(b[0]["X+".Length..]), + int.Parse(b[1]["Y+".Length..]), + int.Parse(p[0]["X=".Length..]), + int.Parse(p[1]["Y=".Length..]) + ); + } + } + + private static long NumberOfPushesToWin((int ax, int ay, int bx, int by, long px, long py) m) { + for (var b = 100; b >= 0; --b) { + if ((m.px - b * m.bx) % m.ax == 0 && (m.py - b * m.by) % m.ay == 0) { + var ax = (m.px - b * m.bx) / m.ax; + var ay = (m.py - b * m.by) / m.ay; + if (ax == ay && ax is >= 0 and <= 100) { + return b + 3 * ax; + } + } + } + + return 0; + } + + private long Z3NumberOfPushesToWin((int ax, int ay, int bx, int by, long px, long py) m) { + var ctx = new Context(); + var solver = ctx.MkSolver(); + + // set up the integer-valued variables + var a = ctx.MkIntConst("a"); + var b = ctx.MkIntConst("b"); + + // set up the equations in the constants + var ax = ctx.MkInt(m.ax); + var ay = ctx.MkInt(m.ay); + var bx = ctx.MkInt(m.bx); + var by = ctx.MkInt(m.by); + var positionX = ctx.MkAdd(ctx.MkMul(a, ax), ctx.MkMul(b, bx)); // a * ax + b * bx + var positionY = ctx.MkAdd(ctx.MkMul(a, ay), ctx.MkMul(b, by)); // a * ay + b * by + + // set up the constraint that it hits the target + var px = ctx.MkInt(m.px); + var py = ctx.MkInt(m.py); + solver.Add(ctx.MkEq(px, positionX)); // x = a * ax + b * bx + solver.Add(ctx.MkEq(py, positionY)); // y = a * ay + b * by + + // ask Z3 to solve the problem + var status = solver.Check(); + + // maybe it can't be done + if (status == Status.UNSATISFIABLE) + return 0; + + // return the price if it can + var model = solver.Model; + var za = Convert.ToInt64(model.Eval(a).ToString()); + var zb = Convert.ToInt64(model.Eval(b).ToString()); + return zb + 3 * za; + } + + protected override long SolvePartOne() => Parse(Input).Sum(NumberOfPushesToWin); + + protected override long SolvePartTwo() => Parse(Input) + .Select(m => m with { px = 10000000000000 + m.px, py = 10000000000000 + m.py }) + .Sum(Z3NumberOfPushesToWin); + + [Fact] + public void ParsesInputCorrectly() { + const string input = + """ + Button A: X+94, Y+34 + Button B: X+22, Y+67 + Prize: X=8400, Y=5400 + """; + var actual = Parse(input).Single(); + + Assert.Equal(94, actual.ax); + Assert.Equal(34, actual.ay); + Assert.Equal(22, actual.bx); + Assert.Equal(67, actual.by); + Assert.Equal(8400, actual.px); + Assert.Equal(5400, actual.py); + } + + [Fact] + public void SolvesEdgeCase() { + const string input = + """ + Button A: X+36, Y+12 + Button B: X+39, Y+91 + Prize: X=2412, Y=1740 + """; + var actual = new Day13(input, Output).SolvePartOne(); + Assert.Equal(174, actual); + } + + [Fact] + public void SolvesPartTwoExample1() { + const string input = + """ + Button A: X+94, Y+34 + Button B: X+22, Y+67 + Prize: X=8400, Y=5400 + """; + var actual = new Day13(input, Output).SolvePartTwo(); + Assert.Equal(0, actual); + } + + [Fact] + public void SolvesPartTwoExample2() { + const string input = + """ + Button A: X+26, Y+66 + Button B: X+67, Y+21 + Prize: X=12748, Y=12176 + """; + var actual = new Day13(input, Output).SolvePartTwo(); + Assert.NotEqual(0, actual); + } + + [Fact] + public void SolvesPartTwoExample3() { + const string input = + """ + Button A: X+17, Y+86 + Button B: X+84, Y+37 + Prize: X=7870, Y=6450 + """; + var actual = new Day13(input, Output).SolvePartTwo(); + Assert.Equal(0, actual); + } + + [Fact] + public void SolvesPartTwoExample4() { + const string input = + """ + Button A: X+69, Y+23 + Button B: X+27, Y+71 + Prize: X=18641, Y=10279 + """; + var actual = new Day13(input, Output).SolvePartTwo(); + Assert.NotEqual(0, actual); + } + + private const string? ExampleInput = @" +Button A: X+94, Y+34 +Button B: X+22, Y+67 +Prize: X=8400, Y=5400 + +Button A: X+26, Y+66 +Button B: X+67, Y+21 +Prize: X=12748, Y=12176 + +Button A: X+17, Y+86 +Button B: X+84, Y+37 +Prize: X=7870, Y=6450 + +Button A: X+69, Y+23 +Button B: X+27, Y+71 +Prize: X=18641, Y=10279 +"; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day13(ExampleInput, Output).SolvePartOne(); + Assert.Equal(480, actual); + } + + [Fact] + public void SolvesPartOneExampleWithFastAlgorithm() { + var actual = Parse(ExampleInput!).Sum(Z3NumberOfPushesToWin); + Assert.Equal(480, actual); + } +} \ No newline at end of file diff --git a/AdventOfCode.CSharp/AdventOfCode.CSharp.csproj b/AdventOfCode.CSharp/AdventOfCode.CSharp.csproj index b282611..2aace72 100644 --- a/AdventOfCode.CSharp/AdventOfCode.CSharp.csproj +++ b/AdventOfCode.CSharp/AdventOfCode.CSharp.csproj @@ -6,6 +6,7 @@ + From 739a8301953615d78e48971948eba74e306ee9f4 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sat, 14 Dec 2024 15:35:04 +0000 Subject: [PATCH 14/43] 2024 day 13 (with algebra) --- AdventOfCode.CSharp/2024/Day13.cs | 109 +++++++++++++++++++----------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day13.cs b/AdventOfCode.CSharp/2024/Day13.cs index 1a6ba11..a443088 100644 --- a/AdventOfCode.CSharp/2024/Day13.cs +++ b/AdventOfCode.CSharp/2024/Day13.cs @@ -9,7 +9,7 @@ namespace AdventOfCode.CSharp._2024; public class Day13 : Solver { public Day13(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } - private static IEnumerable<(int ax, int ay, int bx, int by, long px, long py)> Parse(string s) { + internal static IEnumerable<(int ax, int ay, int bx, int by, long px, long py)> Parse(string s) { var blocks = s.SplitBy("\n\n").Select(Shared.Split); foreach (var block in blocks) { var a = block[0]["Button A: ".Length..].Split(", "); @@ -26,7 +26,7 @@ public Day13(string? input = null, ITestOutputHelper? outputHelper = null) : bas } } - private static long NumberOfPushesToWin((int ax, int ay, int bx, int by, long px, long py) m) { + private static long SearchNumberOfPushesToWin((int ax, int ay, int bx, int by, long px, long py) m) { for (var b = 100; b >= 0; --b) { if ((m.px - b * m.bx) % m.ax == 0 && (m.py - b * m.by) % m.ay == 0) { var ax = (m.px - b * m.bx) / m.ax; @@ -39,48 +39,37 @@ private static long NumberOfPushesToWin((int ax, int ay, int bx, int by, long px return 0; } + + private static long CalculateNumberOfPushesToWin((int ax, int ay, int bx, int by, long px, long py) m) { + /* + * px = a * ax + b * bx + * py = a * ay + b * by + * + * px * by = a * ax * by + b * bx * by multiply (1) by [by] + * py * bx = a * ay * bx + b * by * bx multiply (2) by [bx] + * px * by - py * bx = a * (ax * by - ay * bx) subtract (4) from (3) + * a = (px * by - py * bx) / (ax * by - ay * bx) rearrange (5) for a + * b = (px - a * ax) / bx rearrange (1) for b + */ - private long Z3NumberOfPushesToWin((int ax, int ay, int bx, int by, long px, long py) m) { - var ctx = new Context(); - var solver = ctx.MkSolver(); - - // set up the integer-valued variables - var a = ctx.MkIntConst("a"); - var b = ctx.MkIntConst("b"); - - // set up the equations in the constants - var ax = ctx.MkInt(m.ax); - var ay = ctx.MkInt(m.ay); - var bx = ctx.MkInt(m.bx); - var by = ctx.MkInt(m.by); - var positionX = ctx.MkAdd(ctx.MkMul(a, ax), ctx.MkMul(b, bx)); // a * ax + b * bx - var positionY = ctx.MkAdd(ctx.MkMul(a, ay), ctx.MkMul(b, by)); // a * ay + b * by - - // set up the constraint that it hits the target - var px = ctx.MkInt(m.px); - var py = ctx.MkInt(m.py); - solver.Add(ctx.MkEq(px, positionX)); // x = a * ax + b * bx - solver.Add(ctx.MkEq(py, positionY)); // y = a * ay + b * by + decimal det = m.ax * m.by - m.ay * m.bx; + if (det == 0) return 0; - // ask Z3 to solve the problem - var status = solver.Check(); - - // maybe it can't be done - if (status == Status.UNSATISFIABLE) - return 0; + var a = (m.px * m.by - m.py * m.bx) / det; + var b = (m.px - a * m.ax) / m.bx; + + // x % 1 == 0 "x is an integer" + if (a >= 0 && b >= 0 && a % 1 == 0 && b % 1 == 0) + return (long)(b + 3 * a); - // return the price if it can - var model = solver.Model; - var za = Convert.ToInt64(model.Eval(a).ToString()); - var zb = Convert.ToInt64(model.Eval(b).ToString()); - return zb + 3 * za; + return 0; } - protected override long SolvePartOne() => Parse(Input).Sum(NumberOfPushesToWin); + protected override long SolvePartOne() => Parse(Input).Sum(SearchNumberOfPushesToWin); protected override long SolvePartTwo() => Parse(Input) .Select(m => m with { px = 10000000000000 + m.px, py = 10000000000000 + m.py }) - .Sum(Z3NumberOfPushesToWin); + .Sum(CalculateNumberOfPushesToWin); [Fact] public void ParsesInputCorrectly() { @@ -183,10 +172,48 @@ public void SolvesPartOneExample() { var actual = new Day13(ExampleInput, Output).SolvePartOne(); Assert.Equal(480, actual); } - - [Fact] - public void SolvesPartOneExampleWithFastAlgorithm() { - var actual = Parse(ExampleInput!).Sum(Z3NumberOfPushesToWin); - Assert.Equal(480, actual); +} + +public class Day13Z3(string? input, ITestOutputHelper? outputHelper = null) : Solver(input, outputHelper) { + private static long SolveNumberOfPushesToWin((int ax, int ay, int bx, int by, long px, long py) m) { + var ctx = new Context(); + var solver = ctx.MkSolver(); + + // set up the integer-valued variables + var a = ctx.MkIntConst("a"); + var b = ctx.MkIntConst("b"); + + // set up the equations in the constants + var ax = ctx.MkInt(m.ax); + var ay = ctx.MkInt(m.ay); + var bx = ctx.MkInt(m.bx); + var by = ctx.MkInt(m.by); + var positionX = ctx.MkAdd(ctx.MkMul(a, ax), ctx.MkMul(b, bx)); // a * ax + b * bx + var positionY = ctx.MkAdd(ctx.MkMul(a, ay), ctx.MkMul(b, by)); // a * ay + b * by + + // set up the constraint that it hits the target + var px = ctx.MkInt(m.px); + var py = ctx.MkInt(m.py); + solver.Add(ctx.MkEq(px, positionX)); // x = a * ax + b * bx + solver.Add(ctx.MkEq(py, positionY)); // y = a * ay + b * by + + // ask Z3 to solve the problem + var status = solver.Check(); + + // maybe it can't be done + if (status == Status.UNSATISFIABLE) + return 0; + + // return the price if it can + var model = solver.Model; + var za = Convert.ToInt64(model.Eval(a).ToString()); + var zb = Convert.ToInt64(model.Eval(b).ToString()); + return zb + 3 * za; } + + protected override long SolvePartOne() => Day13.Parse(Input).Sum(SolveNumberOfPushesToWin); + + protected override long SolvePartTwo() => Day13.Parse(Input) + .Select(m => m with { px = 10000000000000 + m.px, py = 10000000000000 + m.py }) + .Sum(SolveNumberOfPushesToWin); } \ No newline at end of file From 748d5a746ef2fa91fbbd6d4c5f2d05056aa7a5a9 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sat, 14 Dec 2024 14:33:06 +0000 Subject: [PATCH 15/43] 2024 day 14 --- AdventOfCode.CSharp/2024/Day14.cs | 170 ++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day14.cs diff --git a/AdventOfCode.CSharp/2024/Day14.cs b/AdventOfCode.CSharp/2024/Day14.cs new file mode 100644 index 0000000..f1b82d7 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day14.cs @@ -0,0 +1,170 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Grid; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day14 : Solver { + public Day14(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { } + + private static (int x, int y, int vx, int vy)[] Parse(string input) { + return + Shared.Split(input) + .Select(l => l.Split(['p', '=', ' ', 'v', ','], + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + .Select(a => (int.Parse(a[0]), int.Parse(a[1]), int.Parse(a[2]), int.Parse(a[3]))) + .ToArray(); + } + + private static void UpdatePositions((int x, int y, int vx, int vy)[] robots, int width, int height) { + for (var i = 0; i < robots.Length; ++i) + robots[i] = robots[i] with { + x = Mathematics.wrap(robots[i].x + robots[i].vx, width), + y = Mathematics.wrap(robots[i].y + robots[i].vy, height) + }; + } + + private static string Render((int x, int y, int vx, int vy)[] robots, int width, int height) { + var grid = new char[height, width]; + grid.Initialize(' '); + + foreach (var robot in robots) + grid[robot.y, robot.x] = + grid[robot.y, robot.x] == ' ' ? '1' : (char)(1 + grid[robot.y, robot.x]); + + return grid.Render(); + } + + private long SafetyFactor((int x, int y, int vx, int vy)[] robots, int width, int height) { + var halfWidth = (width - 1) / 2; + var halfHeight = (height - 1) / 2; + + var upLeft = 0; + var upRight = 0; + var downLeft = 0; + var downRight = 0; + + foreach (var robot in robots) { + if (robot.x < halfWidth && robot.y < halfHeight) + upLeft++; + else if (robot.x > halfWidth && robot.y < halfHeight) + upRight++; + else if (robot.x < halfWidth && robot.y > halfHeight) + downLeft++; + else if (robot.x > halfWidth && robot.y > halfHeight) + downRight++; + } + + return upLeft * upRight * downLeft * downRight; + } + + protected override long SolvePartOne() => SolvePartOne(101, 103); + + private long SolvePartOne(int width, int height) { + var robots = Parse(Input); + + for (var t = 0; t < 100; ++t) + UpdatePositions(robots, width, height); + + Trace.WriteLine(Render(robots, width, height)); + Trace.WriteLine(string.Empty); + + return SafetyFactor(robots, width, height); + } + + protected override long SolvePartTwo() { + var robots = Parse(Input); + + var set = new HashSet<(int x, int y)>(); + var t = 0; + + while (t++ < 10_000) { + UpdatePositions(robots, 101, 103); + + set.Clear(); + foreach (var robot in robots) + set.Add((robot.x, robot.y)); + + var foundTree = set.Any( + /* + * a Christmas tree looks roughly like: + * + * 1 + * 111 + * 11111 + * 1 + * + * look for the top + * + */ + p => + // 111 line + set.Contains((p.x - 1, p.y + 1)) + && set.Contains((p.x, p.y + 1)) + && set.Contains((p.x + 1, p.y + 1)) + // 11111 line + && set.Contains((p.x - 2, p.y + 2)) + && set.Contains((p.x - 1, p.y + 2)) + && set.Contains((p.x, p.y + 2)) + && set.Contains((p.x + 2, p.y + 2)) + && set.Contains((p.x + 2, p.y + 2)) + ); + + if (!foundTree) + continue; + + Trace.WriteLine(Render(robots, 101, 103)); + return t; + } + + throw new InsufficientlyFestiveException(); + } + + private class InsufficientlyFestiveException : InvalidOperationException {} + + private const string? ExampleInput = @" +p=0,4 v=3,-3 +p=6,3 v=-1,-3 +p=10,3 v=-1,2 +p=2,0 v=2,-1 +p=0,0 v=1,3 +p=3,0 v=-2,-2 +p=7,6 v=-1,-3 +p=3,0 v=-1,-2 +p=9,3 v=2,3 +p=7,3 v=-1,2 +p=2,4 v=2,-3 +p=9,5 v=-3,-3 +"; + + [Fact] + public void SingleRobotMovesAsExpected() { + const string? input = "p=2,4 v=2,-3"; + const int width = 11; + const int height = 7; + var robots = Parse(input); + + Trace.WriteLine("Initial state:"); + Trace.WriteLine(Render(robots, width, height)); + Trace.WriteLine(string.Empty); + + for (var t = 1; t < 6; ++t) { + UpdatePositions(robots, width, height); + + Trace.WriteLine(t == 1 ? "After 1 second:" : $"After {t} seconds:"); + Trace.WriteLine(Render(robots, width, height)); + Trace.WriteLine(string.Empty); + } + + Assert.Single(robots); + Assert.Equal(1, robots[0].x); + Assert.Equal(3, robots[0].y); + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day14(ExampleInput, Output).SolvePartOne(11, 7); + Assert.Equal(12, actual); + } +} \ No newline at end of file From b3c3ae07ac8a1a8b954c026e2c54860fc3027bd9 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sun, 15 Dec 2024 10:03:39 +0000 Subject: [PATCH 16/43] 2024 day 15 part 1 --- AdventOfCode.CSharp/2024/Day15.cs | 175 ++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day15.cs diff --git a/AdventOfCode.CSharp/2024/Day15.cs b/AdventOfCode.CSharp/2024/Day15.cs new file mode 100644 index 0000000..b3bd46a --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day15.cs @@ -0,0 +1,175 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Grid; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day15(string? input = null, ITestOutputHelper? outputHelper = null) + : Solver(input, outputHelper) { + private static (char[,] grid, Directions[] moves) Parse(string input) { + var parts = input.SplitBy("\n\n"); + + var grid = parts[0].SplitGrid(); + + var moves = parts[1].Replace("\n", string.Empty).Select(s => s switch { + '^' => Directions.North, + '>' => Directions.East, + 'v' => Directions.South, + '<' => Directions.West, + _ => throw new FormatException($"Invalid direction: {s}") + }).ToArray(); + + return (grid, moves); + } + + private static (int x, int y) AttemptMove((int x, int y) position, Directions direction, char[,] grid) { + (int x, int y) vector = direction switch { + Directions.North => (0, -1), + Directions.East => (1, 0), + Directions.South => (0, 1), + Directions.West => (-1, 0), + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + + var newPosition = (x: position.x + vector.x, y: position.y + vector.y); + + // facing a wall, can't move + if (grid.At(newPosition) == '#') + return position; + + // move the shunt pointer to the end of the stack of boxes + var shunt = newPosition; + while (grid.At(shunt) != '.') { + shunt = (x: shunt.x + vector.x, y: shunt.y + vector.y); + + // can't shunt into a wall + if (grid.At(shunt) == '#') + return position; + } + + // now backtrack + while (shunt != position) { + var nextShunt = (x: shunt.x - vector.x, y: shunt.y - vector.y); + grid[shunt.y, shunt.x] = grid.At(nextShunt); + shunt = nextShunt; + } + + grid[position.y, position.x] = '.'; + return newPosition; + } + + private static int SumGps(char[,] grid) => grid.Flatten((x, y, c) => c != 'O' ? 0 : 100 * y + x).Sum(); + + protected override long SolvePartOne() { + var (grid, moves) = Parse(Input); + var robotPosition = grid.Find('@'); + + foreach (var move in moves) + robotPosition = AttemptMove(robotPosition, move, grid); + + Trace.WriteLine(grid.Render()); + return SumGps(grid); + } + + protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + private readonly char[] directionChars = ['^', '>', 'v', '<']; + + private enum Directions { + North, + East, + South, + West + } + + private const string SimpleExampleInput = + """ + ######## + #..O.O.# + ##@.O..# + #...O..# + #.#.O..# + #...O..# + #......# + ######## + + <^^>>>vv>v<< + """; + + private const string? ExampleInput = + """ + ########## + #..O..O.O# + #......O.# + #.OO..O.O# + #..O@..O.# + #O#..O...# + #O..O..O.# + #.OO.O.OO# + #....O...# + ########## + + ^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^ + vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv< + <>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^ + ^><^><>>><>^^<<^^v>>><^^>v>>>^v><>^v><<<>vvvv>^<><<>^>< + ^>><>^v<><^vvv<^^<><^v<<<><<<^^<^>>^<<<^>>^v^>>^v>vv>^<<^v<>><<><<>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^ + <><^^>^^^<>^vv<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<> + ^^>vv<^v^v^<>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<>< + v^^>>><<^^<>>^v^v^<<>^<^v^v><^<<<><<^vv>>v>v^<<^ + """; + + [Fact] + public void CalculatesSumGpsAsExpected() { + const string gpsExample = + """ + ####### + #...O.. + #...... + """; + + var grid = gpsExample.SplitGrid(); + Assert.Equal(104, SumGps(grid)); + } + + [Fact] + public void RobotMovesAsExpectedForSimpleExample() { + var (grid, moves) = Parse(SimpleExampleInput); + var robotPosition = grid.Find('@'); + + Output.WriteLine("Initial state:"); + Output.WriteLine(grid.Render()); + + foreach (var move in moves) { + robotPosition = AttemptMove(robotPosition, move, grid); + + Output.WriteLine(string.Empty); + Output.WriteLine($"Move {directionChars[(int)move]}:"); + Output.WriteLine(grid.Render()); + } + + const string expected = + """ + ######## + #....OO# + ##.....# + #.....O# + #.#O@..# + #...O..# + #...O..# + ######## + """; + + Assert.Equal(expected, grid.Render()); + Assert.Equal(2028, SumGps(grid)); + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day15(ExampleInput, Output).SolvePartOne(); + Assert.Equal(10092, actual); + } +} \ No newline at end of file From 48468b05f45134e354ae864634438d3124547233 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sun, 15 Dec 2024 11:35:35 +0000 Subject: [PATCH 17/43] 2024 day 15 part 2 --- AdventOfCode.CSharp/2024/Day15.cs | 258 +++++++++++++++++++++++++++--- 1 file changed, 240 insertions(+), 18 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day15.cs b/AdventOfCode.CSharp/2024/Day15.cs index b3bd46a..715f8eb 100644 --- a/AdventOfCode.CSharp/2024/Day15.cs +++ b/AdventOfCode.CSharp/2024/Day15.cs @@ -23,6 +23,12 @@ private static (char[,] grid, Directions[] moves) Parse(string input) { return (grid, moves); } + private static string Expand(string? input) => input == null ? string.Empty : + input.Replace("#", "##") + .Replace("O", "[]") + .Replace(".", "..") + .Replace("@", "@."); + private static (int x, int y) AttemptMove((int x, int y) position, Directions direction, char[,] grid) { (int x, int y) vector = direction switch { Directions.North => (0, -1), @@ -59,7 +65,61 @@ private static (int x, int y) AttemptMove((int x, int y) position, Directions di return newPosition; } - private static int SumGps(char[,] grid) => grid.Flatten((x, y, c) => c != 'O' ? 0 : 100 * y + x).Sum(); + private static (int x, int y) AttemptDoubleWidthMove((int x, int y) position, Directions direction, char[,] grid) { + // left and right moves behave like part 1 + if (direction is Directions.East or Directions.West) + return AttemptMove(position, direction, grid); + + var dy = direction switch { + Directions.North => -1, + Directions.South => 1, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + + var shunts = new List<(int x, int y)>(); + var shuntFront = new HashSet<(int x, int y)> { position }; + + while (true) { + // move the shunt front forwards + var nextPositions = shuntFront.Select(p => p with { y = p.y + dy }).ToArray(); + + // look for obstructions + if (nextPositions.Any(np => grid.At(np) == '#')) + return position; + + // path is not obstructed, advance + shunts.AddRange(shuntFront); + shuntFront.Clear(); + + // if path is completely clear, we are done + if (nextPositions.All(np => grid.At(np) == '.')) + break; + + // otherwise we need to build the next shunt front + foreach (var nextPosition in nextPositions) { + if (grid.At(nextPosition) == '.') + continue; + + shuntFront.Add(nextPosition); + + // check for half-boxes + if (grid.At(nextPosition) == ']') + shuntFront.Add(nextPosition with { x = nextPosition.x - 1 }); + else if (grid.At(nextPosition) == '[') + shuntFront.Add(nextPosition with { x = nextPosition.x + 1 }); + } + } + + // now backtrack + foreach (var shunt in shunts.OrderByDescending(s => s.y * dy)) { + grid[shunt.y + dy, shunt.x] = grid.At(shunt); + grid[shunt.y, shunt.x] = '.'; + } + + return position with { y = position.y + dy }; + } + + private static int SumGps(char[,] grid) => grid.Flatten((x, y, c) => c != 'O' && c != '[' ? 0 : 100 * y + x).Sum(); protected override long SolvePartOne() { var (grid, moves) = Parse(Input); @@ -72,8 +132,25 @@ protected override long SolvePartOne() { return SumGps(grid); } - protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + protected override long SolvePartTwo() { + var (grid, moves) = Parse(Expand(Input)); + var robotPosition = grid.Find('@'); + + Trace.WriteLine("Initial state:"); + Trace.WriteLine(grid.Render()); + + foreach (var move in moves) { + robotPosition = AttemptDoubleWidthMove(robotPosition, move, grid); + + Trace.WriteLine(string.Empty); + Trace.WriteLine($"Move {directionChars[(int)move]}:"); + Trace.WriteLine(grid.Render()); + } + Trace.WriteLine(grid.Render()); + return SumGps(grid); + } + private readonly char[] directionChars = ['^', '>', 'v', '<']; private enum Directions { @@ -82,21 +159,7 @@ private enum Directions { South, West } - - private const string SimpleExampleInput = - """ - ######## - #..O.O.# - ##@.O..# - #...O..# - #.#.O..# - #...O..# - #......# - ######## - - <^^>>>vv>v<< - """; - + private const string? ExampleInput = """ ########## @@ -135,9 +198,56 @@ public void CalculatesSumGpsAsExpected() { Assert.Equal(104, SumGps(grid)); } + [Fact] + public void CalculatesDoubleWidthSumGpsAsExpected1() { + const string gpsExample = + """ + ########## + ##...[]... + ##........ + """; + + var grid = gpsExample.SplitGrid(); + Assert.Equal(105, SumGps(grid)); + } + + [Fact] + public void CalculatesDoubleWidthSumGpsAsExpected2() { + const string gpsExample = + """ + #################### + ##[].......[].[][]## + ##[]...........[].## + ##[]........[][][]## + ##[]......[]....[]## + ##..##......[]....## + ##..[]............## + ##..@......[].[][]## + ##......[][]..[]..## + #################### + """; + + var grid = gpsExample.SplitGrid(); + Assert.Equal(9021, SumGps(grid)); + } + [Fact] public void RobotMovesAsExpectedForSimpleExample() { - var (grid, moves) = Parse(SimpleExampleInput); + const string simpleExampleInput = + """ + ######## + #..O.O.# + ##@.O..# + #...O..# + #.#.O..# + #...O..# + #......# + ######## + + <^^>>>vv>v<< + """; + + var (grid, moves) = Parse(simpleExampleInput); var robotPosition = grid.Find('@'); Output.WriteLine("Initial state:"); @@ -172,4 +282,116 @@ public void SolvesPartOneExample() { var actual = new Day15(ExampleInput, Output).SolvePartOne(); Assert.Equal(10092, actual); } + + [Fact] + public void ExpandsGridAsExpected() { + var expandedInput = Expand(ExampleInput); + var (grid, _) = Parse(expandedInput); + + const string expected = + """ + #################### + ##....[]....[]..[]## + ##............[]..## + ##..[][]....[]..[]## + ##....[]@.....[]..## + ##[]##....[]......## + ##[]....[]....[]..## + ##..[][]..[]..[][]## + ##........[]......## + #################### + """; + + Assert.Equal(expected, grid.Render()); + } + + [Fact] + public void RobotMovesAsExpectedForPartTwoExample() { + const string simpleExampleInput = + """ + ####### + #...#.# + #.....# + #..OO@# + #..O..# + #.....# + ####### + + Date: Mon, 16 Dec 2024 20:10:37 +0000 Subject: [PATCH 18/43] 2024 day 16 part 1 --- AdventOfCode.CSharp/2024/Day16.cs | 144 ++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day16.cs diff --git a/AdventOfCode.CSharp/2024/Day16.cs b/AdventOfCode.CSharp/2024/Day16.cs new file mode 100644 index 0000000..837b230 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day16.cs @@ -0,0 +1,144 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Algorithms; +using AdventOfCode.Core.Grid; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day16 : Solver { + private readonly char[,] grid; + + private readonly (int x, int y) endTile; + + private readonly (int x, int y, Directions heading) initialState; + + public Day16(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { + // test-runner initializes without values: suppress exceptions + if (input == null) { + grid = new char[0,0]; + endTile = (0, 0); + initialState = (0, 0, Directions.East); + return; + } + + grid = input.SplitGrid(); + var startTile = grid.Find('S'); + initialState = (startTile.x, startTile.y, heading: Directions.East); + endTile = grid.Find('E'); + } + + protected override long SolvePartOne() { + var (minCost, path) = Algorithm.AStar(initialState, Connections, Heuristic, IsGoal); + + foreach (var step in path) + grid[step.y, step.x] = step.heading switch { + Directions.North => '^', + Directions.East => '>', + Directions.South => 'v', + Directions.West => '<', + _ => throw new ArgumentOutOfRangeException() + }; + + Trace.WriteLine(grid.Render()); + + return minCost; + } + + protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + private IEnumerable<((int x, int y, Directions heading), int)> Connections( + (int x, int y, Directions heading) state) { + + var forward = state.heading switch { + Directions.North => state with { y = state.y - 1}, + Directions.East => state with { x = state.x + 1 }, + Directions.South => state with { y = state.y + 1 }, + Directions.West => state with { x = state.x - 1 }, + _ => throw new ArgumentOutOfRangeException() + }; + + if (grid.At(forward.x, forward.y) != '#') + yield return (forward, 1); + + yield return (state with { heading = Clockwise(state.heading)}, 1000); + yield return (state with { heading = CounterClockwise(state.heading)}, 1000); + } + + private static Directions Clockwise(Directions direction) => + direction switch { + Directions.North => Directions.East, + Directions.East => Directions.South, + Directions.South => Directions.West, + Directions.West => Directions.North, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + + private static Directions CounterClockwise(Directions direction) => + direction switch { + Directions.North => Directions.West, + Directions.West => Directions.South, + Directions.South => Directions.East, + Directions.East => Directions.North, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + + private int Heuristic((int x, int y, Directions heading) state) => + Math.Abs(endTile.x - state.x) + Math.Abs(endTile.y - state.y); + + private bool IsGoal((int x, int y, Directions heading) state) => + state.x == endTile.x && state.y == endTile.y; + + private enum Directions { + North, + East, + South, + West + } + + private const string? ExampleInput1 = @" +############### +#.......#....E# +#.#.###.#.###.# +#.....#.#...#.# +#.###.#####.#.# +#.#.#.......#.# +#.#.#####.###.# +#...........#.# +###.#.#####.#.# +#...#.....#.#.# +#.#.#.###.#.#.# +#.....#...#.#.# +#.###.#.#.#.#.# +#S..#.....#...# +############### +"; + + private const string? ExampleInput2 = @" +################# +#...#...#...#..E# +#.#.#.#.#.#.#.#.# +#.#.#.#...#...#.# +#.#.#.#.###.#.#.# +#...#.#.#.....#.# +#.#.#.#.#.#####.# +#.#...#.#.#.....# +#.#.#####.#.###.# +#.#.#.......#...# +#.#.###.#####.### +#.#.#...#.....#.# +#.#.#.#####.###.# +#.#.#.........#.# +#.#.#.#########.# +#S#.............# +################# +"; + + [Theory] + [InlineData(ExampleInput1, 7036)] + [InlineData(ExampleInput2, 11048)] + public void SolvesPartOneExample(string input, int expected) { + var actual = new Day16(input, Output).SolvePartOne(); + Assert.Equal(expected, actual); + } +} \ No newline at end of file From 490c66d0d0cb06b6a92578a058b9314b5d50865f Mon Sep 17 00:00:00 2001 From: James Holwell Date: Mon, 16 Dec 2024 21:09:22 +0000 Subject: [PATCH 19/43] 2024 day 16 part 2 --- AdventOfCode.CSharp/2024/Day16.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/AdventOfCode.CSharp/2024/Day16.cs b/AdventOfCode.CSharp/2024/Day16.cs index 837b230..9f6d119 100644 --- a/AdventOfCode.CSharp/2024/Day16.cs +++ b/AdventOfCode.CSharp/2024/Day16.cs @@ -45,8 +45,18 @@ protected override long SolvePartOne() { return minCost; } - protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + protected override long SolvePartTwo() { + var paths = Algorithm.AllStars(initialState, Connections, Heuristic, IsGoal); + var tilesOnAtLeastOneBestPath = paths.SelectMany(p => p).DistinctBy(p => (p.x, p.y)).ToArray(); + + foreach (var tile in tilesOnAtLeastOneBestPath ) + grid[tile.y, tile.x] = 'O'; + Trace.WriteLine(grid.Render()); + + return tilesOnAtLeastOneBestPath.Length; + } + private IEnumerable<((int x, int y, Directions heading), int)> Connections( (int x, int y, Directions heading) state) { @@ -141,4 +151,12 @@ public void SolvesPartOneExample(string input, int expected) { var actual = new Day16(input, Output).SolvePartOne(); Assert.Equal(expected, actual); } + + [Theory] + [InlineData(ExampleInput1, 45)] + [InlineData(ExampleInput2, 64)] + public void SolvesPartTwoExample(string input, int expected) { + var actual = new Day16(input, Output).SolvePartTwo(); + Assert.Equal(expected, actual); + } } \ No newline at end of file From ea0d4f09e8dfa3ba84508d4bfd7bbf3e82889d75 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 17 Dec 2024 12:16:08 +0000 Subject: [PATCH 20/43] 2024 day 17 part 1 --- AdventOfCode.CSharp/2024/Day17.cs | 181 ++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day17.cs diff --git a/AdventOfCode.CSharp/2024/Day17.cs b/AdventOfCode.CSharp/2024/Day17.cs new file mode 100644 index 0000000..61e3893 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day17.cs @@ -0,0 +1,181 @@ +// ReSharper disable InconsistentNaming +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day17(string? input = null, ITestOutputHelper? outputHelper = null) + : Solver(input, outputHelper) { + private int registerA; + private int registerB; + private int registerC; + private readonly List output = []; + private int[] program = []; + private int pointer; + + private static (int registerA, int registerB, int registerC, int[] program) Parse(string input) { + var lines = Shared.Split(input); + return ( + int.Parse(lines[0]["Register A: ".Length..]), + int.Parse(lines[1]["Register B: ".Length..]), + int.Parse(lines[2]["Register C: ".Length..]), + lines[4]["Program: ".Length..].SplitInt(",")); + } + + private void exec(Instructions instruction, int operand) { + switch (instruction) { + case Instructions.adv: + registerA /= 1 << combo(operand); + break; + + case Instructions.bxl: + registerB ^= operand; + break; + + case Instructions.bst: + registerB = combo(operand) % 8; + break; + + case Instructions.jnz: + if (registerA == 0) + break; + + pointer = operand; + return; + + case Instructions.bxc: + registerB ^= registerC; + break; + + case Instructions.@out: + output.Add(combo(operand) % 8); + break; + + case Instructions.bdv: + registerB = registerA / (1 << combo(operand)); + break; + + case Instructions.cdv: + registerC = registerA / (1 << combo(operand)); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + pointer += 2; + return; + + int combo(int code) => code switch { + 4 => registerA, + 5 => registerB, + 6 => registerC, + 7 => throw new InvalidOperationException(), + _ => code + }; + } + + protected override string SolvePartOne() { + (registerA, registerB, registerC, program) = Parse(Input); + + while (true) { + if (pointer > program.Length - 2) + break; + + var instruction = (Instructions)program[pointer]; + var operand = program[pointer + 1]; + exec(instruction, operand); + } + + return string.Join(",", output); + } + + protected override string SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + public enum Instructions : byte { + adv = 0, + bxl = 1, + bst = 2, + jnz = 3, + bxc = 4, + @out = 5, + bdv = 6, + cdv = 7 + } + + private const string ExampleInput = + """ + Register A: 729 + Register B: 0 + Register C: 0 + + Program: 0,1,5,4,3,0 + """; + + [Fact] + public void ParsesInputCorrectly() { + var actual = Parse(ExampleInput); + Assert.Equal(729, actual.registerA); + Assert.Equal(0, actual.registerB); + Assert.Equal(0, actual.registerC); + Assert.Equal([0, 1, 5, 4, 3, 0], actual.program); + } + + [Theory] + [InlineData(Instructions.adv, 2, 256, 0, 0, 256 / 4, 0, 0)] + [InlineData(Instructions.adv, 5, 643, 3, 0, 643 / 8, 3, 0)] + [InlineData(Instructions.bdv, 2, 256, 0, 0, 256, 256 / 4, 0)] + [InlineData(Instructions.bdv, 5, 643, 3, 0, 643, 643 / 8, 0)] + [InlineData(Instructions.cdv, 2, 256, 0, 0, 256, 0, 256 / 4)] + [InlineData(Instructions.cdv, 5, 643, 3, 0, 643, 3, 643 / 8)] + public void CalculatesDivisionCorrectly(Instructions instruction, int operand, int a, int b, int c, int expectedA, int expectedB, int expectedC) { + registerA = a; + registerB = b; + registerC = c; + + exec(instruction, operand); + + Assert.Equal(expectedA, registerA); + Assert.Equal(expectedB, registerB); + Assert.Equal(expectedC, registerC); + } + + [Theory] + [InlineData(0, 0, 9, "2,6", null, 1, null, null)] + [InlineData(10, 0, 0, "5,0,5,1,5,4", null, null, null, "0,1,2")] + [InlineData(2024, 0, 0, "0,1,5,4,3,0", 0, null, null, "4,2,5,6,7,7,7,7,3,1,0")] + [InlineData(0, 29, 0, "1,7", null, 26, null, null)] + [InlineData(0, 2024, 43690, "4,0", null, 44354, null, null)] + public void CalculatesExamples(int a, int b, int c, string prog, int? expectedA, int? expectedB, int? expectedC, string? expectedOutput) { + var input = + $""" + Register A: {a} + Register B: {b} + Register C: {c} + + Program: {prog} + """; + + var solver = new Day17(input, Output); + var actual = solver.SolvePartOne(); + + if (expectedA.HasValue) + Assert.Equal(expectedA, solver.registerA); + + if (expectedB.HasValue) + Assert.Equal(expectedB, solver.registerB); + + if (expectedC.HasValue) + Assert.Equal(expectedC, solver.registerC); + + if (expectedOutput != null) + Assert.Equal(expectedOutput, actual); + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day17(ExampleInput, Output).SolvePartOne(); + Assert.Equal("4,6,3,5,6,3,5,2,1,0", actual); + } +} \ No newline at end of file From 8194e0e761ce7a4441bee91749869cb8464864ab Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 17 Dec 2024 12:55:05 +0000 Subject: [PATCH 21/43] 2024 day 17 part 1 (int->long) --- AdventOfCode.CSharp/2024/Day17.cs | 37 ++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day17.cs b/AdventOfCode.CSharp/2024/Day17.cs index 61e3893..d729ab5 100644 --- a/AdventOfCode.CSharp/2024/Day17.cs +++ b/AdventOfCode.CSharp/2024/Day17.cs @@ -7,9 +7,9 @@ namespace AdventOfCode.CSharp._2024; public class Day17(string? input = null, ITestOutputHelper? outputHelper = null) : Solver(input, outputHelper) { - private int registerA; - private int registerB; - private int registerC; + private long registerA; + private long registerB; + private long registerC; private readonly List output = []; private int[] program = []; private int pointer; @@ -26,7 +26,7 @@ private static (int registerA, int registerB, int registerC, int[] program) Pars private void exec(Instructions instruction, int operand) { switch (instruction) { case Instructions.adv: - registerA /= 1 << combo(operand); + registerA /= 1 << (int)combo(operand); break; case Instructions.bxl: @@ -49,15 +49,15 @@ private void exec(Instructions instruction, int operand) { break; case Instructions.@out: - output.Add(combo(operand) % 8); + output.Add((int)(combo(operand) % 8)); break; case Instructions.bdv: - registerB = registerA / (1 << combo(operand)); + registerB = registerA / (1 << (int)combo(operand)); break; case Instructions.cdv: - registerC = registerA / (1 << combo(operand)); + registerC = registerA / (1 << (int)combo(operand)); break; default: @@ -67,7 +67,7 @@ private void exec(Instructions instruction, int operand) { pointer += 2; return; - int combo(int code) => code switch { + long combo(int code) => code switch { 4 => registerA, 5 => registerB, 6 => registerC, @@ -113,6 +113,15 @@ public enum Instructions : byte { Program: 0,1,5,4,3,0 """; + private const string ExampleInputPartTwo = + """ + Register A: 2024 + Register B: 0 + Register C: 0 + + Program: 0,3,5,4,3,0 + """; + [Fact] public void ParsesInputCorrectly() { var actual = Parse(ExampleInput); @@ -161,13 +170,13 @@ public void CalculatesExamples(int a, int b, int c, string prog, int? expectedA, var actual = solver.SolvePartOne(); if (expectedA.HasValue) - Assert.Equal(expectedA, solver.registerA); + Assert.Equal(expectedA, (int)solver.registerA); if (expectedB.HasValue) - Assert.Equal(expectedB, solver.registerB); + Assert.Equal(expectedB, (int)solver.registerB); if (expectedC.HasValue) - Assert.Equal(expectedC, solver.registerC); + Assert.Equal(expectedC, (int)solver.registerC); if (expectedOutput != null) Assert.Equal(expectedOutput, actual); @@ -178,4 +187,10 @@ public void SolvesPartOneExample() { var actual = new Day17(ExampleInput, Output).SolvePartOne(); Assert.Equal("4,6,3,5,6,3,5,2,1,0", actual); } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day17(ExampleInputPartTwo, Output).SolvePartTwo(); + Assert.Equal("117440", actual); + } } \ No newline at end of file From 64b18d3975146c68123ef727e0319a5714e4af34 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 17 Dec 2024 15:35:14 +0000 Subject: [PATCH 22/43] 2024 day 17 part 1 (tracing) --- AdventOfCode.CSharp/2024/Day17.cs | 56 +++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day17.cs b/AdventOfCode.CSharp/2024/Day17.cs index d729ab5..ed6b147 100644 --- a/AdventOfCode.CSharp/2024/Day17.cs +++ b/AdventOfCode.CSharp/2024/Day17.cs @@ -76,16 +76,58 @@ private void exec(Instructions instruction, int operand) { }; } + private string PrepareTraceMessage() { + var a = registerA; + var b = registerB; + var c = registerC; + var ap = string.Join(string.Empty, program.Select((p, i) => + i == pointer ? $"[{p} " : + i == pointer + 1 ? $"{p}]" : + i % 2 == 0 ? $" {p} " : $"{p} ")); + if (ap.Length < 10) ap += new string(' ', 10-ap.Length); + + var instruction = (Instructions)program[pointer]; + var operand = program[pointer + 1]; + var comboOperand = operand switch { + 4 => "a", + 5 => "b", + 6 => "c", + 7 => "!!!", + _ => $"{operand}" + }; + + var narrative = instruction switch { + Instructions.adv when operand is >=4 and <= 6 => $"a := a / 2^{comboOperand})", + Instructions.adv => $"a := a / {1 << operand}", + Instructions.bxl => $"b := b ^ {operand}", + Instructions.bst => $"b := {comboOperand} % 8", + Instructions.jnz when registerA == 0 => "quit", + Instructions.jnz => $"jmp {operand}", + Instructions.bxc => $"b := b ^ c", + Instructions.@out => $"out {comboOperand} % 8", + Instructions.bdv => $"b := a / (1 << {comboOperand})", + Instructions.cdv => $"c := a / (1 << {comboOperand})", + _ => string.Empty + }; + + return + $"{ap} " + + $"{instruction.ToString()} {operand} " + + $"{narrative}{new string(' ', narrative.Length > 20 ? 0 : 20 - narrative.Length)}" + + $"| {a,10} | {b,10} | {c,10} | "; + } + protected override string SolvePartOne() { (registerA, registerB, registerC, program) = Parse(Input); - while (true) { - if (pointer > program.Length - 2) - break; - - var instruction = (Instructions)program[pointer]; - var operand = program[pointer + 1]; - exec(instruction, operand); + Trace.WriteLine($"program{new string(' ', program.Length < 4 ? 3 : (program.Length * 5)/2 - 7)} op narrative | register a | register b | register c | output"); + Trace.WriteLine($"-------{new string('-', program.Length < 4 ? 3 : (program.Length * 5)/2 - 7)}-------------------------------|------------|------------|------------|-----------------"); + while (pointer < program.Length) { + var traceMessage = PrepareTraceMessage(); + + exec((Instructions)program[pointer], program[pointer + 1]); + + Trace.WriteLine(traceMessage + string.Join(",", output)); } return string.Join(",", output); From 13cf361383e757151be8c0607aa8159ae9d3c7b8 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 17 Dec 2024 23:02:04 +0000 Subject: [PATCH 23/43] 2024 day 17 part 1 (cleaner shifting) --- AdventOfCode.CSharp/2024/Day17.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day17.cs b/AdventOfCode.CSharp/2024/Day17.cs index ed6b147..38228b8 100644 --- a/AdventOfCode.CSharp/2024/Day17.cs +++ b/AdventOfCode.CSharp/2024/Day17.cs @@ -26,7 +26,7 @@ private static (int registerA, int registerB, int registerC, int[] program) Pars private void exec(Instructions instruction, int operand) { switch (instruction) { case Instructions.adv: - registerA /= 1 << (int)combo(operand); + registerA >>= (int)combo(operand); break; case Instructions.bxl: @@ -53,11 +53,11 @@ private void exec(Instructions instruction, int operand) { break; case Instructions.bdv: - registerB = registerA / (1 << (int)combo(operand)); + registerB = registerA >> (int)combo(operand); break; case Instructions.cdv: - registerC = registerA / (1 << (int)combo(operand)); + registerC = registerA >> (int)combo(operand); break; default: @@ -97,16 +97,16 @@ private string PrepareTraceMessage() { }; var narrative = instruction switch { - Instructions.adv when operand is >=4 and <= 6 => $"a := a / 2^{comboOperand})", - Instructions.adv => $"a := a / {1 << operand}", + Instructions.adv when operand is >= 4 and <= 6 => $"a := a >> {comboOperand}", + Instructions.adv => $"a := a >> {operand}", Instructions.bxl => $"b := b ^ {operand}", Instructions.bst => $"b := {comboOperand} % 8", Instructions.jnz when registerA == 0 => "quit", Instructions.jnz => $"jmp {operand}", Instructions.bxc => $"b := b ^ c", Instructions.@out => $"out {comboOperand} % 8", - Instructions.bdv => $"b := a / (1 << {comboOperand})", - Instructions.cdv => $"c := a / (1 << {comboOperand})", + Instructions.bdv => $"b := a >> {comboOperand}", + Instructions.cdv => $"c := a >> {comboOperand}", _ => string.Empty }; From 21bc187dc3c600abb526f1205475e694496ef21a Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 17 Dec 2024 13:03:37 +0000 Subject: [PATCH 24/43] 2024 day 17 part 2 (bfs haha) --- AdventOfCode.CSharp/2024/Day17.cs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/AdventOfCode.CSharp/2024/Day17.cs b/AdventOfCode.CSharp/2024/Day17.cs index 38228b8..929faf2 100644 --- a/AdventOfCode.CSharp/2024/Day17.cs +++ b/AdventOfCode.CSharp/2024/Day17.cs @@ -133,7 +133,27 @@ protected override string SolvePartOne() { return string.Join(",", output); } - protected override string SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + protected override string SolvePartTwo() { + (_, _, _, program) = Parse(Input); + var i = -1L; + + while (!output.SequenceEqual(program)) { + registerA = ++i; + registerB = 0; + registerC = 0; + pointer = 0; + output.Clear(); + + while (true) { + if (pointer > program.Length - 1) + break; + + exec((Instructions)program[pointer], program[pointer + 1]); + } + } + + return i.ToString(); + } public enum Instructions : byte { adv = 0, From 8921fa60558c2811e468462156526e4f254ad201 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 17 Dec 2024 22:29:50 +0000 Subject: [PATCH 25/43] 2024 day 17 part 2 --- AdventOfCode.CSharp/2024/Day17.cs | 122 +++++++++++++++++++----------- 1 file changed, 78 insertions(+), 44 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day17.cs b/AdventOfCode.CSharp/2024/Day17.cs index 929faf2..50ce701 100644 --- a/AdventOfCode.CSharp/2024/Day17.cs +++ b/AdventOfCode.CSharp/2024/Day17.cs @@ -1,4 +1,5 @@ // ReSharper disable InconsistentNaming + using AdventOfCode.Core; using Xunit; using Xunit.Abstractions; @@ -17,9 +18,9 @@ public class Day17(string? input = null, ITestOutputHelper? outputHelper = null) private static (int registerA, int registerB, int registerC, int[] program) Parse(string input) { var lines = Shared.Split(input); return ( - int.Parse(lines[0]["Register A: ".Length..]), - int.Parse(lines[1]["Register B: ".Length..]), - int.Parse(lines[2]["Register C: ".Length..]), + int.Parse(lines[0]["Register A: ".Length..]), + int.Parse(lines[1]["Register B: ".Length..]), + int.Parse(lines[2]["Register C: ".Length..]), lines[4]["Program: ".Length..].SplitInt(",")); } @@ -38,12 +39,12 @@ private void exec(Instructions instruction, int operand) { break; case Instructions.jnz: - if (registerA == 0) + if (registerA == 0) break; pointer = operand; return; - + case Instructions.bxc: registerB ^= registerC; break; @@ -51,7 +52,7 @@ private void exec(Instructions instruction, int operand) { case Instructions.@out: output.Add((int)(combo(operand) % 8)); break; - + case Instructions.bdv: registerB = registerA >> (int)combo(operand); break; @@ -59,7 +60,7 @@ private void exec(Instructions instruction, int operand) { case Instructions.cdv: registerC = registerA >> (int)combo(operand); break; - + default: throw new ArgumentOutOfRangeException(); } @@ -80,11 +81,11 @@ private string PrepareTraceMessage() { var a = registerA; var b = registerB; var c = registerC; - var ap = string.Join(string.Empty, program.Select((p, i) => + var ap = string.Join(string.Empty, program.Select((p, i) => i == pointer ? $"[{p} " : - i == pointer + 1 ? $"{p}]" : + i == pointer + 1 ? $"{p}]" : i % 2 == 0 ? $" {p} " : $"{p} ")); - if (ap.Length < 10) ap += new string(' ', 10-ap.Length); + if (ap.Length < 10) ap += new string(' ', 10 - ap.Length); var instruction = (Instructions)program[pointer]; var operand = program[pointer + 1]; @@ -111,22 +112,22 @@ private string PrepareTraceMessage() { }; return - $"{ap} " + + $"{ap} " + $"{instruction.ToString()} {operand} " + $"{narrative}{new string(' ', narrative.Length > 20 ? 0 : 20 - narrative.Length)}" + $"| {a,10} | {b,10} | {c,10} | "; } - + protected override string SolvePartOne() { (registerA, registerB, registerC, program) = Parse(Input); - Trace.WriteLine($"program{new string(' ', program.Length < 4 ? 3 : (program.Length * 5)/2 - 7)} op narrative | register a | register b | register c | output"); - Trace.WriteLine($"-------{new string('-', program.Length < 4 ? 3 : (program.Length * 5)/2 - 7)}-------------------------------|------------|------------|------------|-----------------"); + Trace.WriteLine($"program{new string(' ', program.Length < 4 ? 3 : (program.Length * 5) / 2 - 7)} op narrative | register a | register b | register c | output"); + Trace.WriteLine($"-------{new string('-', program.Length < 4 ? 3 : (program.Length * 5) / 2 - 7)}-------------------------------|------------|------------|------------|-----------------"); while (pointer < program.Length) { var traceMessage = PrepareTraceMessage(); - + exec((Instructions)program[pointer], program[pointer + 1]); - + Trace.WriteLine(traceMessage + string.Join(",", output)); } @@ -135,24 +136,57 @@ protected override string SolvePartOne() { protected override string SolvePartTwo() { (_, _, _, program) = Parse(Input); - var i = -1L; - - while (!output.SequenceEqual(program)) { - registerA = ++i; - registerB = 0; - registerC = 0; - pointer = 0; - output.Clear(); + var targetDigit = program.Length - 1; + long accumulator = -1; + Trace.WriteLine($"> program: {string.Join(",", program)}"); + + while (targetDigit >= 0) { while (true) { - if (pointer > program.Length - 1) - break; + ++accumulator; + registerA = accumulator; + registerB = 0; + registerC = 0; + pointer = 0; + output.Clear(); - exec((Instructions)program[pointer], program[pointer + 1]); + while (pointer < program.Length) + exec((Instructions)program[pointer], program[pointer + 1]); + + if (output[0] != program[targetDigit]) + continue; + + if (output.SequenceEqual(program[^output.Count..])) + break; } + + Trace.WriteLine($"{accumulator,16}: {new string(' ', 2 * (program.Length - output.Count))}{string.Join(",", output)}"); + + if (program.SequenceEqual(output)) + break; + + // by analysing the program in part 1, we know that a := a / 8 each cycle + if (output.SequenceEqual(program[^output.Count..])) + accumulator = accumulator * 8 - 1; + + --targetDigit; } - return i.ToString(); + Trace.WriteLine(string.Empty); + Trace.WriteLine($"Checking {accumulator}"); + Trace.WriteLine("Expected: " + string.Join(",", program)); + + registerA = accumulator; + registerB = 0; + registerC = 0; + pointer = 0; + output.Clear(); + + while (pointer < program.Length) + exec((Instructions)program[pointer], program[pointer + 1]); + + Trace.WriteLine("Actual : " + string.Join(",", output)); + return program.SequenceEqual(output) ? accumulator.ToString() : "oof."; } public enum Instructions : byte { @@ -166,24 +200,24 @@ public enum Instructions : byte { cdv = 7 } - private const string ExampleInput = + private const string ExampleInput = """ Register A: 729 Register B: 0 Register C: 0 - + Program: 0,1,5,4,3,0 """; - private const string ExampleInputPartTwo = + private const string ExampleInputPartTwo = """ Register A: 2024 Register B: 0 Register C: 0 - + Program: 0,3,5,4,3,0 """; - + [Fact] public void ParsesInputCorrectly() { var actual = Parse(ExampleInput); @@ -192,7 +226,7 @@ public void ParsesInputCorrectly() { Assert.Equal(0, actual.registerC); Assert.Equal([0, 1, 5, 4, 3, 0], actual.program); } - + [Theory] [InlineData(Instructions.adv, 2, 256, 0, 0, 256 / 4, 0, 0)] [InlineData(Instructions.adv, 5, 643, 3, 0, 643 / 8, 3, 0)] @@ -204,14 +238,14 @@ public void CalculatesDivisionCorrectly(Instructions instruction, int operand, i registerA = a; registerB = b; registerC = c; - + exec(instruction, operand); - + Assert.Equal(expectedA, registerA); Assert.Equal(expectedB, registerB); Assert.Equal(expectedC, registerC); } - + [Theory] [InlineData(0, 0, 9, "2,6", null, 1, null, null)] [InlineData(10, 0, 0, "5,0,5,1,5,4", null, null, null, "0,1,2")] @@ -219,7 +253,7 @@ public void CalculatesDivisionCorrectly(Instructions instruction, int operand, i [InlineData(0, 29, 0, "1,7", null, 26, null, null)] [InlineData(0, 2024, 43690, "4,0", null, 44354, null, null)] public void CalculatesExamples(int a, int b, int c, string prog, int? expectedA, int? expectedB, int? expectedC, string? expectedOutput) { - var input = + var input = $""" Register A: {a} Register B: {b} @@ -230,26 +264,26 @@ public void CalculatesExamples(int a, int b, int c, string prog, int? expectedA, var solver = new Day17(input, Output); var actual = solver.SolvePartOne(); - + if (expectedA.HasValue) Assert.Equal(expectedA, (int)solver.registerA); - + if (expectedB.HasValue) Assert.Equal(expectedB, (int)solver.registerB); - + if (expectedC.HasValue) Assert.Equal(expectedC, (int)solver.registerC); - + if (expectedOutput != null) Assert.Equal(expectedOutput, actual); } - + [Fact] public void SolvesPartOneExample() { var actual = new Day17(ExampleInput, Output).SolvePartOne(); Assert.Equal("4,6,3,5,6,3,5,2,1,0", actual); } - + [Fact] public void SolvesPartTwoExample() { var actual = new Day17(ExampleInputPartTwo, Output).SolvePartTwo(); From d0c0e51c4c2271e84edeff43fb8ca238e3adfb42 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Wed, 18 Dec 2024 09:56:07 +0000 Subject: [PATCH 26/43] 2024 day 18 part 1 --- AdventOfCode.CSharp/2024/Day18.cs | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day18.cs diff --git a/AdventOfCode.CSharp/2024/Day18.cs b/AdventOfCode.CSharp/2024/Day18.cs new file mode 100644 index 0000000..67e5540 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day18.cs @@ -0,0 +1,85 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Algorithms; +using AdventOfCode.Core.Grid; +using AdventOfCode.Core.Points; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day18(string? input = null, ITestOutputHelper? outputHelper = null) + : Solver(input, outputHelper) { + private static (int x, int y)[] Parse(string input) => + Shared.Split(input) + .Select(s => s.Split(',')) + .Select(s => (int.Parse(s[0]), int.Parse(s[1]))) + .ToArray(); + + private long SolvePartOne(string input, int numberOfBytes, int width, int height) { + var coords = Parse(input); + + var grid = new char[height, width]; + grid.Initialize('.'); + + for (var i = 0; i < numberOfBytes; i++) + grid[coords[i].y, coords[i].x] = '#'; + + Trace.WriteLine(grid.Render()); + + var traversableAddresses = new HashSet<(int, int)>(grid.Where(c => c == '.')); + + var d = Algorithm.Dijkstra( + startNode: (0, 0), + nodes: traversableAddresses, + labelFunc: p => p, + connectionFunc: TraversableNeighbors); + + return d[(width - 1, height - 1)]; + + IEnumerable<((int x, int y), int d)> TraversableNeighbors((int x, int y) p) { + if (traversableAddresses.Contains(p.Up())) yield return (p.Up(), 1); + if (traversableAddresses.Contains(p.Right())) yield return (p.Right(), 1); + if (traversableAddresses.Contains(p.Down())) yield return (p.Down(), 1); + if (traversableAddresses.Contains(p.Left())) yield return (p.Left(), 1); + } + } + + protected override long SolvePartOne() => SolvePartOne(Input, 1024, 71, 71); + + protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + private const string ExampleInput = + """ + 5,4 + 4,2 + 4,5 + 3,0 + 2,1 + 6,3 + 2,4 + 1,5 + 0,6 + 3,3 + 2,6 + 5,1 + 1,2 + 5,5 + 2,5 + 6,5 + 1,4 + 0,4 + 6,4 + 1,1 + 6,1 + 1,0 + 0,5 + 1,6 + 2,0 + """; + + [Fact] + public void SolvesPartOneExample() { + var actual = SolvePartOne(ExampleInput, 12, 7, 7); + Assert.Equal(22, actual); + } +} \ No newline at end of file From 262568af2e319894bea24547929c7a645f060421 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Wed, 18 Dec 2024 10:13:44 +0000 Subject: [PATCH 27/43] 2024 day 18 part 2 --- AdventOfCode.CSharp/2024/Day18.cs | 54 ++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day18.cs b/AdventOfCode.CSharp/2024/Day18.cs index 67e5540..d922e5d 100644 --- a/AdventOfCode.CSharp/2024/Day18.cs +++ b/AdventOfCode.CSharp/2024/Day18.cs @@ -8,21 +8,19 @@ namespace AdventOfCode.CSharp._2024; public class Day18(string? input = null, ITestOutputHelper? outputHelper = null) - : Solver(input, outputHelper) { + : Solver(input, outputHelper) { private static (int x, int y)[] Parse(string input) => Shared.Split(input) .Select(s => s.Split(',')) .Select(s => (int.Parse(s[0]), int.Parse(s[1]))) .ToArray(); - private long SolvePartOne(string input, int numberOfBytes, int width, int height) { - var coords = Parse(input); - + private long SolvePartOne((int x, int y)[] bytes, int numberOfBytes, int width, int height) { var grid = new char[height, width]; grid.Initialize('.'); for (var i = 0; i < numberOfBytes; i++) - grid[coords[i].y, coords[i].x] = '#'; + grid[bytes[i].y, bytes[i].x] = '#'; Trace.WriteLine(grid.Render()); @@ -44,9 +42,43 @@ private long SolvePartOne(string input, int numberOfBytes, int width, int height } } - protected override long SolvePartOne() => SolvePartOne(Input, 1024, 71, 71); + private string SolvePartTwo((int x, int y)[] bytes, int width, int height) { + var traversableAddresses = new HashSet<(int, int)>( + Enumerable.Range(0, width).SelectMany(x => Enumerable.Range(0, height).Select(y => (x, y)))); + + for (var i = 0; i < bytes.Length; ++i) { + traversableAddresses.Remove((bytes[i].x, bytes[i].y)); - protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + var d = Algorithm.Dijkstra( + startNode: (0, 0), + nodes: traversableAddresses, + labelFunc: p => p, + connectionFunc: TraversableNeighbors); + + if (d[(width - 1, height - 1)] == int.MaxValue) + return $"{bytes[i].x},{bytes[i].y}"; + } + + return "oof."; + + IEnumerable<((int x, int y), int d)> TraversableNeighbors((int x, int y) p) { + var up = p.Up(); + if (traversableAddresses.Contains(up)) yield return (up, 1); + + var right = p.Right(); + if (traversableAddresses.Contains(right)) yield return (right, 1); + + var down = p.Down(); + if (traversableAddresses.Contains(down)) yield return (down, 1); + + var left = p.Left(); + if (traversableAddresses.Contains(left)) yield return (left, 1); + } + } + + protected override string SolvePartOne() => SolvePartOne(Parse(Input), 1024, 71, 71).ToString(); + + protected override string SolvePartTwo() => SolvePartTwo(Parse(Input), 71, 71); private const string ExampleInput = """ @@ -79,7 +111,13 @@ private long SolvePartOne(string input, int numberOfBytes, int width, int height [Fact] public void SolvesPartOneExample() { - var actual = SolvePartOne(ExampleInput, 12, 7, 7); + var actual = SolvePartOne(Parse(ExampleInput), 12, 7, 7); Assert.Equal(22, actual); } + + [Fact] + public void SolvesPartTwoExample() { + var actual = SolvePartTwo(Parse(ExampleInput), 7, 7); + Assert.Equal("6,1", actual); + } } \ No newline at end of file From 525f90d4bee9595a52a51e7cee9be32cf37f9316 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Thu, 19 Dec 2024 13:02:53 +0000 Subject: [PATCH 28/43] 2024 day 19 part 1 --- AdventOfCode.CSharp/2024/Day19.cs | 112 ++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day19.cs diff --git a/AdventOfCode.CSharp/2024/Day19.cs b/AdventOfCode.CSharp/2024/Day19.cs new file mode 100644 index 0000000..6147185 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day19.cs @@ -0,0 +1,112 @@ +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day19 : Solver { + private readonly string[] patterns; + + private readonly string[] designs; + + private readonly HashSet possibleCache = []; + + private readonly HashSet impossibleCache = []; + + public Day19(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { + if (input == null) { + // allow empty constructor from test runner + patterns = designs = []; + return; + } + + var parts = input.SplitBy("\n\n"); + patterns = parts[0].Split(", "); + designs = parts[1].Split('\n'); + } + + private bool IsPossible(string design) { + if (possibleCache.Contains(design)) return true; + if (impossibleCache.Contains(design)) return false; + + var isPossible = IsPossibleInner(design); + + if (isPossible) + possibleCache.Add(design); + + if (!isPossible) + impossibleCache.Add(design); + + return isPossible; + } + + private bool IsPossibleInner(string design) { + for (int i = 0, ci = patterns.Length; i < ci; ++i) { + var pattern = patterns[i]; + + if (pattern == design) + return true; + + if (pattern.Length > design.Length) + continue; + + for (int j = 0, cj = pattern.Length; j < cj; ++j) + if (pattern[j] != design[j]) + goto NoMatch; + + if (IsPossible(design[pattern.Length..])) + return true; + + NoMatch: ; + } + + return false; + } + + protected override long SolvePartOne() => designs.Count(IsPossible); + + protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + private const string ExampleInput = + """ + r, wr, b, g, bwu, rb, gb, br + + brwrr + bggr + gbbr + rrbgbr + ubwu + bwurrg + brgr + bbrgwb + """; + + [Fact] + public void ParseWorks() { + var solver = new Day19(ExampleInput); + + // ReSharper disable StringLiteralTypo + var expectedPatterns = new[] {"r", "wr", "b", "g", "bwu", "rb", "gb", "br"}; + var expectedDesigns = new[] + { + "brwrr", + "bggr", + "gbbr", + "rrbgbr", + "ubwu", + "bwurrg", + "brgr", + "bbrgwb" + }; + // ReSharper restore StringLiteralTypo + + Assert.Equal(expectedPatterns, solver.patterns); + Assert.Equal(expectedDesigns, solver.designs); + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day19(ExampleInput, Output).SolvePartOne(); + Assert.Equal(6, actual); + } +} \ No newline at end of file From 8694b9a4a3c25d58360f97aec7cda3b4929e5d04 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Thu, 19 Dec 2024 13:08:43 +0000 Subject: [PATCH 29/43] 2024 day 19 part 2 --- AdventOfCode.CSharp/2024/Day19.cs | 42 ++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/AdventOfCode.CSharp/2024/Day19.cs b/AdventOfCode.CSharp/2024/Day19.cs index 6147185..6e958d1 100644 --- a/AdventOfCode.CSharp/2024/Day19.cs +++ b/AdventOfCode.CSharp/2024/Day19.cs @@ -13,6 +13,8 @@ public class Day19 : Solver { private readonly HashSet impossibleCache = []; + private readonly Dictionary possibilitiesCache = new(); + public Day19(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { if (input == null) { // allow empty constructor from test runner @@ -63,9 +65,41 @@ private bool IsPossibleInner(string design) { return false; } + private long Possibilities(string design) { + if (possibilitiesCache.TryGetValue(design, out var count)) + return count; + + return possibilitiesCache[design] = PossibilitiesInner(design); + } + + private long PossibilitiesInner(string design) { + var possibilities = 0L; + + for (int i = 0, ci = patterns.Length; i < ci; ++i) { + var pattern = patterns[i]; + + if (pattern == design) { + possibilities++; + continue; + } + + if (pattern.Length > design.Length) + continue; + + for (int j = 0, cj = pattern.Length; j < cj; ++j) + if (pattern[j] != design[j]) + goto NoMatch; + + possibilities += Possibilities(design[pattern.Length..]); + NoMatch: ; + } + + return possibilities; + } + protected override long SolvePartOne() => designs.Count(IsPossible); - protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + protected override long SolvePartTwo() => designs.Sum(Possibilities); private const string ExampleInput = """ @@ -109,4 +143,10 @@ public void SolvesPartOneExample() { var actual = new Day19(ExampleInput, Output).SolvePartOne(); Assert.Equal(6, actual); } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day19(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(16, actual); + } } \ No newline at end of file From 7906ed8f1a1d156f3aab21a6c442edb8fb1684ce Mon Sep 17 00:00:00 2001 From: James Holwell Date: Fri, 20 Dec 2024 14:11:59 +0000 Subject: [PATCH 30/43] 2024 day 20 part 1 --- AdventOfCode.CSharp/2024/Day20.cs | 103 ++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day20.cs diff --git a/AdventOfCode.CSharp/2024/Day20.cs b/AdventOfCode.CSharp/2024/Day20.cs new file mode 100644 index 0000000..dce82ee --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day20.cs @@ -0,0 +1,103 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Algorithms; +using AdventOfCode.Core.Grid; +using AdventOfCode.Core.Points; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day20(string? input = null, ITestOutputHelper? outputHelper = null) + : Solver(input, outputHelper) { + protected override long SolvePartOne() { + var grid = Input.SplitGrid(); + var start = grid.Find('S'); + var end = grid.Find('E'); + var coordinates = grid.Where(c => c == '.').Union([start, end]).ToArray(); + var spaces = new HashSet<(int,int)>(coordinates); + + var dijkstra = Algorithm.Dijkstra(start, coordinates, p => p, ConnectionFunc); + + var fastestTimeWithoutCheating = dijkstra[end]; + Trace.WriteLine($"Fastest time without cheating: {fastestTimeWithoutCheating}"); + + /* + * Cheats look like: + * + * 2 + * 2 # 2 + * 2 # 0 # 2 + * 2 # 2 + * 2 + * + * Where 0 is the 'start position', '#' is the first pico-second through a wall + * and 2 is the second pico-second re-entering the track. + * + * If you don't glitch through a wall, it's not a very good cheat + * + */ + var possibleCheats = coordinates.SelectMany(CheatOptions).ToArray(); + Trace.WriteLine($"Discovered {possibleCheats.Length} possible cheats"); + + var cheatSavings = possibleCheats + .Select(pc => (cheat: pc, savings: dijkstra[pc.end] - dijkstra[pc.start] - 2)) + .Where(cs => cs.savings > 0) + .ToArray(); + + foreach (var group in cheatSavings.GroupBy(ch => ch.savings).OrderBy(g => g.Key)) + Trace.WriteLine($" - There are {group.Count()} cheats that save {group.Key} picoseconds."); + + return cheatSavings.Count(cs => cs.savings >= 100); + + IEnumerable<((int x, int y), int cost)> ConnectionFunc((int x, int y) node) { + if (spaces.Contains(node.Up())) yield return (node.Up(), 1); + if (spaces.Contains(node.Right())) yield return (node.Right(), 1); + if (spaces.Contains(node.Down())) yield return (node.Down(), 1); + if (spaces.Contains(node.Left())) yield return (node.Left(), 1); + } + + IEnumerable<((int x, int y) start, (int x, int y) end)> CheatOptions((int x, int y) node) { + var upIsWall = !spaces.Contains(node.Up()); + var rightIsWall = !spaces.Contains(node.Right()); + var downIsWall = !spaces.Contains(node.Down()); + var leftIsWall = !spaces.Contains(node.Left()); + + // clockwise from north + if (upIsWall && spaces.Contains(node with {y = node.y - 2})) yield return (node, node with {y = node.y - 2}); + if ((upIsWall || rightIsWall) && spaces.Contains((node.x + 1, node.y - 1))) yield return (node, (node.x + 1, node.y - 1)); + if (rightIsWall && spaces.Contains(node with { x = node.x + 2})) yield return (node, node with {x = node.x + 2}); + if ((rightIsWall || downIsWall) && spaces.Contains((node.x + 1, node.y + 1))) yield return (node, (node.x + 1, node.y + 1)); + if (downIsWall && spaces.Contains(node with {y = node.y + 2})) yield return (node, node with { y = node.y + 2 }); + if ((downIsWall || leftIsWall) && spaces.Contains((node.x - 1, node.y + 1))) yield return (node, (node.x - 1, node.y + 1)); + if (leftIsWall && spaces.Contains(node with { x = node.x - 2})) yield return (node, node with {x = node.x - 2}); + if ((leftIsWall || upIsWall) && spaces.Contains((node.x - 1, node.y - 1))) yield return (node, (node.x - 1, node.y - 1)); + } + } + + protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + private const string? ExampleInput = + """ + ############### + #...#...#.....# + #.#.#.#.#.###.# + #S#...#.#.#...# + #######.#.#.### + #######.#.#...# + #######.#.###.# + ###..E#...#...# + ###.#######.### + #...###...#...# + #.#####.#.###.# + #.#...#.#.#...# + #.#.#.#.#.#.### + #...#...#...### + ############### + """; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day20(ExampleInput, Output).SolvePartOne(); + Assert.Equal(0, actual); + } +} \ No newline at end of file From fd8a6faf4f7bde7216207b1f75e5329cef08b560 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Fri, 20 Dec 2024 14:38:10 +0000 Subject: [PATCH 31/43] 2024 day 20 part 2 --- AdventOfCode.CSharp/2024/Day20.cs | 115 +++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 32 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day20.cs b/AdventOfCode.CSharp/2024/Day20.cs index dce82ee..88f5d39 100644 --- a/AdventOfCode.CSharp/2024/Day20.cs +++ b/AdventOfCode.CSharp/2024/Day20.cs @@ -7,19 +7,56 @@ namespace AdventOfCode.CSharp._2024; -public class Day20(string? input = null, ITestOutputHelper? outputHelper = null) - : Solver(input, outputHelper) { - protected override long SolvePartOne() { - var grid = Input.SplitGrid(); +public class Day20 : Solver { + private readonly HashSet<(int x, int y)> spaces; + + private readonly Dictionary<(int x, int y), int> dijkstra; + + private int minimumCheatSavingsForResult = 100; + + private readonly int fastestTime; + + public Day20(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { + if (input == null) { + spaces = new HashSet<(int, int)>(); + dijkstra = new Dictionary<(int, int), int>(); + return; + } + + var grid = input.SplitGrid(); var start = grid.Find('S'); var end = grid.Find('E'); var coordinates = grid.Where(c => c == '.').Union([start, end]).ToArray(); - var spaces = new HashSet<(int,int)>(coordinates); - var dijkstra = Algorithm.Dijkstra(start, coordinates, p => p, ConnectionFunc); + spaces = [..coordinates]; + dijkstra = Algorithm.Dijkstra(start, coordinates, p => p, ConnectionFunc); + fastestTime = dijkstra[end]; - var fastestTimeWithoutCheating = dijkstra[end]; - Trace.WriteLine($"Fastest time without cheating: {fastestTimeWithoutCheating}"); + return; + + IEnumerable<((int x, int y), int cost)> ConnectionFunc((int x, int y) node) { + if (spaces.Contains(node.Up())) yield return (node.Up(), 1); + if (spaces.Contains(node.Right())) yield return (node.Right(), 1); + if (spaces.Contains(node.Down())) yield return (node.Down(), 1); + if (spaces.Contains(node.Left())) yield return (node.Left(), 1); + } + } + + protected override long SolvePartOne() { + Trace.WriteLine($"Fastest time without cheating: {fastestTime}"); + + var possibleCheats = spaces.SelectMany(CheatOptions).ToArray(); + Trace.WriteLine($"Discovered {possibleCheats.Length} possible cheats"); + + var cheatSavings = possibleCheats + .Select(pc => (cheat: pc, savings: dijkstra[pc.end] - dijkstra[pc.start] - 2)) + .Where(cs => cs.savings > 0) + .ToArray(); + + foreach (var group in cheatSavings.GroupBy(ch => ch.savings).OrderBy(g => g.Key)) + Trace.WriteLine($" - There are {group.Count()} cheats that save {group.Key} picoseconds."); + + return cheatSavings.Count(cs => cs.savings >= minimumCheatSavingsForResult); /* * Cheats look like: @@ -34,28 +71,7 @@ protected override long SolvePartOne() { * and 2 is the second pico-second re-entering the track. * * If you don't glitch through a wall, it's not a very good cheat - * */ - var possibleCheats = coordinates.SelectMany(CheatOptions).ToArray(); - Trace.WriteLine($"Discovered {possibleCheats.Length} possible cheats"); - - var cheatSavings = possibleCheats - .Select(pc => (cheat: pc, savings: dijkstra[pc.end] - dijkstra[pc.start] - 2)) - .Where(cs => cs.savings > 0) - .ToArray(); - - foreach (var group in cheatSavings.GroupBy(ch => ch.savings).OrderBy(g => g.Key)) - Trace.WriteLine($" - There are {group.Count()} cheats that save {group.Key} picoseconds."); - - return cheatSavings.Count(cs => cs.savings >= 100); - - IEnumerable<((int x, int y), int cost)> ConnectionFunc((int x, int y) node) { - if (spaces.Contains(node.Up())) yield return (node.Up(), 1); - if (spaces.Contains(node.Right())) yield return (node.Right(), 1); - if (spaces.Contains(node.Down())) yield return (node.Down(), 1); - if (spaces.Contains(node.Left())) yield return (node.Left(), 1); - } - IEnumerable<((int x, int y) start, (int x, int y) end)> CheatOptions((int x, int y) node) { var upIsWall = !spaces.Contains(node.Up()); var rightIsWall = !spaces.Contains(node.Right()); @@ -74,7 +90,28 @@ protected override long SolvePartOne() { } } - protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + protected override long SolvePartTwo() { + var possibleCheats = spaces.SelectMany(CheatOptions).ToArray(); + Trace.WriteLine($"Discovered {possibleCheats.Length} possible cheats"); + + var cheatSavings = possibleCheats + .Select(pc => (cheat: pc, savings: dijkstra[pc.end] - dijkstra[pc.start] - pc.distance)) + .Where(cs => cs.savings > 0) + .ToArray(); + + foreach (var group in cheatSavings.GroupBy(ch => ch.savings).OrderBy(g => g.Key)) + Trace.WriteLine($" - There are {group.Count()} cheats that save {group.Key} picoseconds."); + + return cheatSavings.Count(cs => cs.savings >= minimumCheatSavingsForResult); + + IEnumerable<((int x, int y) start, (int x, int y) end, int distance)> CheatOptions((int x, int y) start) { + return spaces + .Where(s => Math.Abs(start.x - s.x) + Math.Abs(start.y - s.y) <= 20) + .Select(s => (end: s, distance: Math.Abs(start.x - s.x) + Math.Abs(start.y - s.y))) + .Select(s => (start, s.end, s.distance)) + .ToArray(); + } + } private const string? ExampleInput = """ @@ -97,7 +134,21 @@ protected override long SolvePartOne() { [Fact] public void SolvesPartOneExample() { - var actual = new Day20(ExampleInput, Output).SolvePartOne(); - Assert.Equal(0, actual); + var solver = new Day20(ExampleInput, Output) { + minimumCheatSavingsForResult = 50 + }; + + var actual = solver.SolvePartOne(); + Assert.Equal(1, actual); + } + + [Fact] + public void SolvesPartTwoExample() { + var solver = new Day20(ExampleInput, Output) { + minimumCheatSavingsForResult = 50 + }; + + var actual = solver.SolvePartTwo(); + Assert.Equal(285, actual); } } \ No newline at end of file From bc2ae9558fd64906bb487ebb66e10da4d610f7e4 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Mon, 23 Dec 2024 10:06:50 +0000 Subject: [PATCH 32/43] 2024 day 20 part 2 (more obvious) --- AdventOfCode.CSharp/2024/Day20.cs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day20.cs b/AdventOfCode.CSharp/2024/Day20.cs index 88f5d39..06233e0 100644 --- a/AdventOfCode.CSharp/2024/Day20.cs +++ b/AdventOfCode.CSharp/2024/Day20.cs @@ -10,7 +10,8 @@ namespace AdventOfCode.CSharp._2024; public class Day20 : Solver { private readonly HashSet<(int x, int y)> spaces; - private readonly Dictionary<(int x, int y), int> dijkstra; + private readonly Dictionary<(int x, int y), int> forward; + private readonly Dictionary<(int x, int y), int> backward; private int minimumCheatSavingsForResult = 100; @@ -19,7 +20,8 @@ public class Day20 : Solver { public Day20(string? input = null, ITestOutputHelper? outputHelper = null) : base(input, outputHelper) { if (input == null) { spaces = new HashSet<(int, int)>(); - dijkstra = new Dictionary<(int, int), int>(); + forward = new Dictionary<(int, int), int>(); + backward = new Dictionary<(int, int), int>(); return; } @@ -29,8 +31,9 @@ public Day20(string? input = null, ITestOutputHelper? outputHelper = null) : bas var coordinates = grid.Where(c => c == '.').Union([start, end]).ToArray(); spaces = [..coordinates]; - dijkstra = Algorithm.Dijkstra(start, coordinates, p => p, ConnectionFunc); - fastestTime = dijkstra[end]; + forward = Algorithm.Dijkstra(start, coordinates, p => p, ConnectionFunc); + backward = Algorithm.Dijkstra(end, coordinates, p => p, ConnectionFunc); + fastestTime = forward[end]; return; @@ -49,12 +52,14 @@ protected override long SolvePartOne() { Trace.WriteLine($"Discovered {possibleCheats.Length} possible cheats"); var cheatSavings = possibleCheats - .Select(pc => (cheat: pc, savings: dijkstra[pc.end] - dijkstra[pc.start] - 2)) + .Select(pc => (cheat: pc, savings: fastestTime - forward[pc.start] - 2 - backward[pc.end])) .Where(cs => cs.savings > 0) .ToArray(); foreach (var group in cheatSavings.GroupBy(ch => ch.savings).OrderBy(g => g.Key)) - Trace.WriteLine($" - There are {group.Count()} cheats that save {group.Key} picoseconds."); + Trace.WriteLine(group.Count() == 1 + ? $" - There is one cheat that saves {group.Key} picoseconds." + : $" - There are {group.Count()} cheats that save {group.Key} picoseconds."); return cheatSavings.Count(cs => cs.savings >= minimumCheatSavingsForResult); @@ -95,12 +100,14 @@ protected override long SolvePartTwo() { Trace.WriteLine($"Discovered {possibleCheats.Length} possible cheats"); var cheatSavings = possibleCheats - .Select(pc => (cheat: pc, savings: dijkstra[pc.end] - dijkstra[pc.start] - pc.distance)) + .Select(pc => (cheat: pc, savings: fastestTime - backward[pc.end] - forward[pc.start] - pc.distance)) .Where(cs => cs.savings > 0) .ToArray(); - foreach (var group in cheatSavings.GroupBy(ch => ch.savings).OrderBy(g => g.Key)) - Trace.WriteLine($" - There are {group.Count()} cheats that save {group.Key} picoseconds."); + foreach (var group in cheatSavings.Where(cs => cs.savings >= minimumCheatSavingsForResult).GroupBy(ch => ch.savings).OrderBy(g => g.Key)) + Trace.WriteLine(group.Count() == 1 + ? $" - There is one cheat that saves {group.Key} picoseconds." + : $" - There are {group.Count()} cheats that save {group.Key} picoseconds."); return cheatSavings.Count(cs => cs.savings >= minimumCheatSavingsForResult); From 6af3d4f27c7c97cc5c117fba23c90849f9116e4d Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sun, 22 Dec 2024 01:17:50 +0000 Subject: [PATCH 33/43] 2024 day 21 part 1 --- AdventOfCode.CSharp/2024/Day21.cs | 227 ++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day21.cs diff --git a/AdventOfCode.CSharp/2024/Day21.cs b/AdventOfCode.CSharp/2024/Day21.cs new file mode 100644 index 0000000..93e663d --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day21.cs @@ -0,0 +1,227 @@ +using AdventOfCode.Core; +using AdventOfCode.Core.Points; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day21(string? input = null, ITestOutputHelper? outputHelper = null) + : Solver(input, outputHelper) { + private readonly ILookup<(char from, char to),string> numericPaths = CreateAllNumericPaths(); + + /// + /// Gets the coordinate of the door button + /// + /// + /// + /// Origin is 'gap' + /// X is + for left to right + /// Y is + for bottom to top + /// + /// +---+---+---+ + /// | 7 | 8 | 9 | + /// +---+---+---+ + /// | 4 | 5 | 6 | + /// +---+---+---+ + /// | 1 | 2 | 3 | + /// +---+---+---+ + /// | 0 | A | + /// +---+---+ + /// + /// + private static readonly Dictionary NumericCoordinates = new() { + { 'A' , (2, 0) }, + { '0' , (1, 0) }, + { '1' , (0, 1) }, + { '2' , (1, 1) }, + { '3' , (2, 1) }, + { '4' , (0, 2) }, + { '5' , (1, 2) }, + { '6' , (2, 2) }, + { '7' , (0, 3) }, + { '8' , (1, 3) }, + { '9' , (2, 3) } + }; + + private static ILookup<(char from, char to), string> CreateAllNumericPaths() { + var inverse = NumericCoordinates.ToDictionary(p => p.Value, p => p.Key); + var particles = new Queue<(char start, (int x, int y) position, string path)>(NumericCoordinates.Select(p => (p.Key, (p.Value), ""))); + var seen = new HashSet<(char start, (int x, int y) position, string path)>(); + + while (particles.TryDequeue(out var particle)) { + if (!seen.Add(particle) || particle.path.Length > 5) continue; + + if (inverse.ContainsKey(particle.position.Up())) // inverted y-axis + particles.Enqueue(particle with { position = particle.position.Up(), path = particle.path + "v" }); + + if (inverse.ContainsKey(particle.position.Right())) + particles.Enqueue(particle with { position = particle.position.Right(), path = particle.path + ">" }); + + if (inverse.ContainsKey(particle.position.Down())) // inverted y-axis + particles.Enqueue(particle with { position = particle.position.Down(), path = particle.path + "^" }); + + if (inverse.ContainsKey(particle.position.Left())) + particles.Enqueue(particle with { position = particle.position.Left(), path = particle.path + "<" }); + } + + var pathLengths = seen.GroupBy(particle => (particle.start, particle.position)) + .ToDictionary(g => g.Key, g => g.Min(gg => gg.path.Length)); + + return seen + .Where(particle => pathLengths[(particle.start, particle.position)] == particle.path.Length) + .OrderBy(particle => particle.start) + .ThenBy(particle => inverse[particle.position]) + .ToLookup(particle => (from: particle.start, to: inverse[particle.position]), particle => particle.path); + } + + private string[] PressesForCode(string code) { + return new[] { 'A' }.Concat(code).Pairwise() + .Aggregate(new [] { string.Empty }, (acc, pair) => + acc.SelectMany(o => numericPaths[(pair.Item1, pair.Item2)].Select(p => o + p + "A")).ToArray()); + } + + /// + /// Gets the possible keypresses for each directional state change + /// + /// + /// +---+---+ + /// | ^ | A | + /// +---+---+---+ + /// | « | v | » | + /// +---+---+---+ + /// + private static string[] DirectionalPresses(char curr, char next) { + if (curr == next) return [string.Empty]; + + switch (curr) { + case 'A' when next == '^': return ["<"]; + case 'A' when next == '<': return ["v<<","': return ["v"]; + + case '^' when next == 'A': return [">"]; + case '^' when next == '<': return ["v<"]; + case '^' when next == 'v': return ["v"]; + case '^' when next == '>': return [">v","v>"]; + + case '<' when next == 'A': return [">>^",">^>"]; + case '<' when next == '^': return [">^"]; + case '<' when next == 'v': return [">"]; + case '<' when next == '>': return [">>"]; + + case 'v' when next == 'A': return [">^","^>"]; + case 'v' when next == '^': return ["^"]; + case 'v' when next == '<': return ["<"]; + case 'v' when next == '>': return [">"]; + + case '>' when next == 'A': return ["^"]; + case '>' when next == '^': return ["<^","^<"]; + case '>' when next == '<': return ["<<"]; + case '>' when next == 'v': return ["<"]; + + default: throw new InvalidOperationException(); + } + } + + private static string[] PressesForPresses(string required) { + return new[] { 'A' }.Concat(required).Pairwise() + .Aggregate(new[] { string.Empty }, (acc, pair) => + acc.SelectMany(o => DirectionalPresses(pair.Item1, pair.Item2).Select(p => o + p + "A")).ToArray()); + } + + private static string YourPressesForPresses(string required) { + return new[] { 'A' }.Concat(required).Pairwise() + .Aggregate(string.Empty, (acc, pair) => + acc + DirectionalPresses(pair.Item1, pair.Item2).Last() + "A"); + } + + private string Solve(string code) { + var shortest = int.MaxValue; + var candidate = string.Empty; + + Trace.WriteLine($"Code: {code}"); + var pressesForCode = PressesForCode(code); + + foreach (var pressesForCodeOption in pressesForCode) { + Trace.WriteLine($"1st Keypad: {pressesForCodeOption}"); + var pressesForPresses = PressesForPresses(pressesForCodeOption); + + foreach (var pressesForPressesOption in pressesForPresses) { + Trace.WriteLine($"2nd Keypad: {pressesForPressesOption}"); + var yourPressesForPresses = YourPressesForPresses(pressesForPressesOption); + + if (yourPressesForPresses.Length < shortest) { + shortest = yourPressesForPresses.Length; + candidate = yourPressesForPresses; + } + } + } + + return candidate; + } + + [Theory] + [InlineData("029A", ">^AvAA<^A>A>^AvA^A^A^A>AAvA^AA>^AAAvA<^A>A")] + [InlineData("980A", ">^AAAvA^A>^AvAA<^A>AA>^AAAvA<^A>A^AA")] + [InlineData("179A", ">^A>^AAvAA<^A>A>^AAvA^A^AAAA>^AAAvA<^A>A")] + [InlineData("456A", ">^AA>^AAvAA<^A>A^AA^AAA>^AAvA<^A>A")] + [InlineData("379A", ">^AvA^A>^AAvA<^A>AAvA^A^AAAA>^AAAvA<^A>A")] + public void SolvesEachLengthOfPartOne(string input, string expected) { + var actual = new string(Solve(input)); + Trace.WriteLine(expected); + Trace.WriteLine(actual); + Assert.Equal(expected.Length, actual.Length); + } + + protected override long SolvePartOne() => Shared.Split(Input).Sum(code => int.Parse(code[..^1]) * Solve(code).Length); + protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + private const string? ExampleInput = + """ + 029A + 980A + 179A + 456A + 379A + """; + + [Fact] + public void GeneratesAllPotentialPaths() { + var solver = new Day21(); + foreach (var pair in solver.numericPaths) { + Trace.WriteLine($"({pair.Key.from} -> {pair.Key.to}): {string.Join(", ", pair)}"); + } + + Assert.NotEmpty(solver.numericPaths); + } + + [Fact] + public void CalculatePressesForNumericKeypadCorrectly() { + var actual = PressesForCode("029A"); + foreach (var option in actual) + Trace.WriteLine(option); + + // ReSharper disable once StringLiteralTypo + const string expected = "^^AvvvA"; + Assert.Contains(expected, actual); + } + + [Theory] + // ReSharper disable StringLiteralTypo + [InlineData("^^AvvvA", "v<>^AAvA<^AA>A^A")] + [InlineData("v<>^AAvA<^AA>A^A", ">^AvAA<^A>A>^AvA^A^A^A>AAvA^AA>^AAAvA<^A>A")] + // ReSharper restore StringLiteralTypo + public void CalculatePressesForDirectionalKeypadCorrectly(string input, string expected) { + var actual = PressesForPresses(input); + foreach (var option in actual) + Trace.WriteLine(option); + + Assert.Contains(expected, actual); + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day21(ExampleInput, Output).SolvePartOne(); + Assert.Equal(126384, actual); + } +} \ No newline at end of file From 84e71ac2c1ba6570abcc5f42d337d486c4d1bb57 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sun, 22 Dec 2024 02:12:12 +0000 Subject: [PATCH 34/43] 2024 day 21 part 2 --- AdventOfCode.CSharp/2024/Day21.cs | 79 ++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day21.cs b/AdventOfCode.CSharp/2024/Day21.cs index 93e663d..0dba1f1 100644 --- a/AdventOfCode.CSharp/2024/Day21.cs +++ b/AdventOfCode.CSharp/2024/Day21.cs @@ -8,6 +8,8 @@ namespace AdventOfCode.CSharp._2024; public class Day21(string? input = null, ITestOutputHelper? outputHelper = null) : Solver(input, outputHelper) { private readonly ILookup<(char from, char to),string> numericPaths = CreateAllNumericPaths(); + + private readonly Dictionary<(char, char, int), long> minimumLengthCache = []; /// /// Gets the coordinate of the door button @@ -160,21 +162,47 @@ private string Solve(string code) { return candidate; } - [Theory] - [InlineData("029A", ">^AvAA<^A>A>^AvA^A^A^A>AAvA^AA>^AAAvA<^A>A")] - [InlineData("980A", ">^AAAvA^A>^AvAA<^A>AA>^AAAvA<^A>A^AA")] - [InlineData("179A", ">^A>^AAvAA<^A>A>^AAvA^A^AAAA>^AAAvA<^A>A")] - [InlineData("456A", ">^AA>^AAvAA<^A>A^AA^AAA>^AAvA<^A>A")] - [InlineData("379A", ">^AvA^A>^AAvA<^A>AAvA^A^AAAA>^AAAvA<^A>A")] - public void SolvesEachLengthOfPartOne(string input, string expected) { - var actual = new string(Solve(input)); - Trace.WriteLine(expected); - Trace.WriteLine(actual); - Assert.Equal(expected.Length, actual.Length); + private long SolvePartTwo(string code) => + PressesForCode(code) + .Min(p => new[] { 'A' }.Concat(p).Pairwise() + .Sum(pair => MinimumLength(pair.Item1, pair.Item2, 25))); + + private long MinimumLength(char curr, char next, int remainingEncodings) { + if (minimumLengthCache.TryGetValue((curr, next, remainingEncodings), out var value)) + return value; + + return minimumLengthCache[(curr, next, remainingEncodings)] = MinimumLengthInner(curr, next, remainingEncodings); } - + + private long MinimumLengthInner(char curr, char next, int remainingEncodings) => + remainingEncodings == 0 ? 1 : + DirectionalPresses(curr, next).Select(d => new[] { 'A' }.Concat(d).Concat(['A']).Pairwise()).Select(steps => steps.Sum(p => MinimumLength(p.Item1, p.Item2, remainingEncodings - 1))).Min(); + + /* + private long DebuggableMinimumLengthInner(char curr, char next, int remainingEncodings) { + if (remainingEncodings == 0) + return 1; + + var directionalPresses = DirectionalPresses(curr, next); + var min = long.MaxValue; + foreach (var d in directionalPresses) { + var length = 0L; + var steps = new[] { 'A' }.Concat(d).Concat(new[] { 'A' }).Pairwise(); + + foreach (var p in steps) + length += MinimumLength(p.Item1, p.Item2, remainingEncodings - 1); + + if (length < min) + min = length; + } + + return min; + } + */ + protected override long SolvePartOne() => Shared.Split(Input).Sum(code => int.Parse(code[..^1]) * Solve(code).Length); - protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + protected override long SolvePartTwo() => Shared.Split(Input).Sum(code => int.Parse(code[..^1]) * SolvePartTwo(code)); private const string? ExampleInput = """ @@ -194,7 +222,7 @@ public void GeneratesAllPotentialPaths() { Assert.NotEmpty(solver.numericPaths); } - + [Fact] public void CalculatePressesForNumericKeypadCorrectly() { var actual = PressesForCode("029A"); @@ -205,7 +233,7 @@ public void CalculatePressesForNumericKeypadCorrectly() { const string expected = "^^AvvvA"; Assert.Contains(expected, actual); } - + [Theory] // ReSharper disable StringLiteralTypo [InlineData("^^AvvvA", "v<>^AAvA<^AA>A^A")] @@ -218,10 +246,29 @@ public void CalculatePressesForDirectionalKeypadCorrectly(string input, string e Assert.Contains(expected, actual); } - + + [Theory] + [InlineData("029A", ">^AvAA<^A>A>^AvA^A^A^A>AAvA^AA>^AAAvA<^A>A")] + [InlineData("980A", ">^AAAvA^A>^AvAA<^A>AA>^AAAvA<^A>A^AA")] + [InlineData("179A", ">^A>^AAvAA<^A>A>^AAvA^A^AAAA>^AAAvA<^A>A")] + [InlineData("456A", ">^AA>^AAvAA<^A>A^AA^AAA>^AAvA<^A>A")] + [InlineData("379A", ">^AvA^A>^AAvA<^A>AAvA^A^AAAA>^AAAvA<^A>A")] + public void SolvesEachLengthOfPartOne(string input, string expected) { + var actual = new string(Solve(input)); + Trace.WriteLine(expected); + Trace.WriteLine(actual); + Assert.Equal(expected.Length, actual.Length); + } + [Fact] public void SolvesPartOneExample() { var actual = new Day21(ExampleInput, Output).SolvePartOne(); Assert.Equal(126384, actual); } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day21(ExampleInput, Output).SolvePartTwo(); + Assert.Equal(154115708116294, actual); + } } \ No newline at end of file From f35fca2d91f9ca0b40f3225f0ddaeffe055aecb0 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Mon, 23 Dec 2024 11:30:24 +0000 Subject: [PATCH 35/43] 2024 day 21 part 2 (faster) --- AdventOfCode.CSharp/2024/Day21.cs | 194 +++++++++++++++++++----------- 1 file changed, 126 insertions(+), 68 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day21.cs b/AdventOfCode.CSharp/2024/Day21.cs index 0dba1f1..621a999 100644 --- a/AdventOfCode.CSharp/2024/Day21.cs +++ b/AdventOfCode.CSharp/2024/Day21.cs @@ -1,3 +1,4 @@ +using System.Text; using AdventOfCode.Core; using AdventOfCode.Core.Points; using Xunit; @@ -10,6 +11,8 @@ public class Day21(string? input = null, ITestOutputHelper? outputHelper = null) private readonly ILookup<(char from, char to),string> numericPaths = CreateAllNumericPaths(); private readonly Dictionary<(char, char, int), long> minimumLengthCache = []; + + private readonly Dictionary<(char, char, int), long> fastCache = []; /// /// Gets the coordinate of the door button @@ -124,48 +127,62 @@ private static string[] DirectionalPresses(char curr, char next) { default: throw new InvalidOperationException(); } } - - private static string[] PressesForPresses(string required) { - return new[] { 'A' }.Concat(required).Pairwise() - .Aggregate(new[] { string.Empty }, (acc, pair) => - acc.SelectMany(o => DirectionalPresses(pair.Item1, pair.Item2).Select(p => o + p + "A")).ToArray()); - } - - private static string YourPressesForPresses(string required) { - return new[] { 'A' }.Concat(required).Pairwise() - .Aggregate(string.Empty, (acc, pair) => - acc + DirectionalPresses(pair.Item1, pair.Item2).Last() + "A"); - } - - private string Solve(string code) { - var shortest = int.MaxValue; - var candidate = string.Empty; - - Trace.WriteLine($"Code: {code}"); - var pressesForCode = PressesForCode(code); - foreach (var pressesForCodeOption in pressesForCode) { - Trace.WriteLine($"1st Keypad: {pressesForCodeOption}"); - var pressesForPresses = PressesForPresses(pressesForCodeOption); + private static string OptimisedDirectionalPresses(char curr, char next) { + return (curr) switch { + 'A' when next == '^' => "A "Av< "A' => "AvA", - foreach (var pressesForPressesOption in pressesForPresses) { - Trace.WriteLine($"2nd Keypad: {pressesForPressesOption}"); - var yourPressesForPresses = YourPressesForPresses(pressesForPressesOption); + '^' when next == 'A' => "A>A", + '^' when next == '<' => "Av "AvA", + '^' when next == '>' => "Av>A", - if (yourPressesForPresses.Length < shortest) { - shortest = yourPressesForPresses.Length; - candidate = yourPressesForPresses; - } - } + '<' when next == 'A' => "A>>^A", + '<' when next == '^' => "A>^A", + '<' when next == 'v' => "A>A", + '<' when next == '>' => "A>>A", + + 'v' when next == 'A' => "A^>A", + 'v' when next == '^' => "A^A", + 'v' when next == '<' => "A' => "A>A", + + '>' when next == 'A' => "A^A", + '>' when next == '^' => "A<^A", + '>' when next == '<' => "A<' when next == 'v' => "A "AA" + }; + } + + private static string PressesForPresses(string required) { + var transitions = new[] { 'A' }.Concat(required).Pairwise(); + + var sb = new StringBuilder(required.Length * 4); + + foreach (var transition in transitions) { + sb.Append(OptimisedDirectionalPresses(transition.Item1, transition.Item2)[1..^1]); + sb.Append('A'); } - return candidate; + return sb.ToString(); } - private long SolvePartTwo(string code) => - PressesForCode(code) - .Min(p => new[] { 'A' }.Concat(p).Pairwise() - .Sum(pair => MinimumLength(pair.Item1, pair.Item2, 25))); + private long FastMinimumLength(char curr, char next, int remainingEncodings) { + if (remainingEncodings == 0 || curr == next) + return 1; + + if (fastCache.TryGetValue((curr, next, remainingEncodings), out long result)) + return result; + + var presses = OptimisedDirectionalPresses(curr, next); + + return fastCache[(curr, next, remainingEncodings)] = presses.Pairwise().Sum(pair => FastMinimumLength(pair.Item1, pair.Item2, remainingEncodings - 1)); + } private long MinimumLength(char curr, char next, int remainingEncodings) { if (minimumLengthCache.TryGetValue((curr, next, remainingEncodings), out var value)) @@ -174,35 +191,49 @@ private long MinimumLength(char curr, char next, int remainingEncodings) { return minimumLengthCache[(curr, next, remainingEncodings)] = MinimumLengthInner(curr, next, remainingEncodings); } + /// + /// Brute-force calculate the minimum length for an encoding + /// + /// + /// + /// + /// + /// Equivalent to the following: + /// + /// + /// private long MinimumLengthInner(char curr, char next, int remainingEncodings) => remainingEncodings == 0 ? 1 : DirectionalPresses(curr, next).Select(d => new[] { 'A' }.Concat(d).Concat(['A']).Pairwise()).Select(steps => steps.Sum(p => MinimumLength(p.Item1, p.Item2, remainingEncodings - 1))).Min(); - /* - private long DebuggableMinimumLengthInner(char curr, char next, int remainingEncodings) { - if (remainingEncodings == 0) - return 1; - - var directionalPresses = DirectionalPresses(curr, next); - var min = long.MaxValue; - foreach (var d in directionalPresses) { - var length = 0L; - var steps = new[] { 'A' }.Concat(d).Concat(new[] { 'A' }).Pairwise(); - - foreach (var p in steps) - length += MinimumLength(p.Item1, p.Item2, remainingEncodings - 1); - - if (length < min) - min = length; - } - - return min; - } - */ + private long Solve(string code, int remainingEncodings) => + PressesForCode(code) + .Min(p => new[] { 'A' }.Concat(p).Pairwise() + .Sum(pair => FastMinimumLength(pair.Item1, pair.Item2, remainingEncodings))); - protected override long SolvePartOne() => Shared.Split(Input).Sum(code => int.Parse(code[..^1]) * Solve(code).Length); + protected override long SolvePartOne() => Shared.Split(Input).Sum(code => int.Parse(code[..^1]) * Solve(code, 2)); - protected override long SolvePartTwo() => Shared.Split(Input).Sum(code => int.Parse(code[..^1]) * SolvePartTwo(code)); + protected override long SolvePartTwo() => Shared.Split(Input).Sum(code => int.Parse(code[..^1]) * Solve(code, 25)); private const string? ExampleInput = """ @@ -236,15 +267,15 @@ public void CalculatePressesForNumericKeypadCorrectly() { [Theory] // ReSharper disable StringLiteralTypo - [InlineData("^^AvvvA", "v<>^AAvA<^AA>A^A")] - [InlineData("v<>^AAvA<^AA>A^A", ">^AvAA<^A>A>^AvA^A^A^A>AAvA^AA>^AAAvA<^A>A")] + [InlineData("^^AvvvA", "v<>^AAvA<^AA>AA")] + [InlineData("v<>^AAvA<^AA>A^A", ">^AvAA<^A>A>^AvA^AA^A>AAvA^AA^>AAAvA<^A>A")] // ReSharper restore StringLiteralTypo public void CalculatePressesForDirectionalKeypadCorrectly(string input, string expected) { + // cheeky scamp, examples aren't optimal for higher number of keypads + expected = expected.Replace("^>", ">>^"); + var actual = PressesForPresses(input); - foreach (var option in actual) - Trace.WriteLine(option); - - Assert.Contains(expected, actual); + Assert.Equal(expected, actual); } [Theory] @@ -254,12 +285,39 @@ public void CalculatePressesForDirectionalKeypadCorrectly(string input, string e [InlineData("456A", ">^AA>^AAvAA<^A>A^AA^AAA>^AAvA<^A>A")] [InlineData("379A", ">^AvA^A>^AAvA<^A>AAvA^A^AAAA>^AAAvA<^A>A")] public void SolvesEachLengthOfPartOne(string input, string expected) { - var actual = new string(Solve(input)); - Trace.WriteLine(expected); - Trace.WriteLine(actual); - Assert.Equal(expected.Length, actual.Length); + var actual = Solve(input, 2); + Assert.Equal(expected.Length, actual); } + [Theory] + [InlineData('A', '<')] + [InlineData('A', 'v')] + [InlineData('^', '>')] + [InlineData('<', 'A')] + [InlineData('v', 'A')] + [InlineData('>', '^')] + public void TestIfBestCandidatesChange(char curr, char next) { + var options = DirectionalPresses(curr, next); + + for (var i = 0; i < 10; ++i) { + var remainingEncodings = i; + var answers = options.Select(option => ( + option, + length: new[] { 'A' }.Concat(option).Concat(['A']).Pairwise().Sum(p => MinimumLength(p.Item1, p.Item2, remainingEncodings)))) + .ToArray(); + + var trace = answers.Select(a => $"{a.option}={a.length}").Aggregate((a, b) => $"{a}, {b}"); + + if (answers.GroupBy(p => p.length).Count() == 1) { + Trace.WriteLine($"For {i} downstream keypads, it's a tie ({trace})"); + continue; + } + + var best = answers.OrderBy(p => p.length).First(); + Trace.WriteLine($"For {i} keypads, {best.option} is best ({trace})"); + } + } + [Fact] public void SolvesPartOneExample() { var actual = new Day21(ExampleInput, Output).SolvePartOne(); From a49ed14df7c78c45b28ebd054e6c0f47ff6c5816 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sun, 22 Dec 2024 10:43:18 +0000 Subject: [PATCH 36/43] 2024 day 22 part 1 --- AdventOfCode.CSharp/2024/Day22.cs | 88 +++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day22.cs diff --git a/AdventOfCode.CSharp/2024/Day22.cs b/AdventOfCode.CSharp/2024/Day22.cs new file mode 100644 index 0000000..0256350 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day22.cs @@ -0,0 +1,88 @@ +using System.Runtime.CompilerServices; +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day22(string? input = null, ITestOutputHelper? outputHelper = null) + : Solver(input, outputHelper) { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long NextSecretNumber(long current) { + // mul 64 (2^5); mix; prune + current = ((current << 6) ^ current) % 16777216; + + // div 32 (2^4); mix; prune + current = ((current >> 5) ^ current) % 16777216; + + // mul 2048 (2^11); mix; prune + current = ((current << 11) ^ current) % 16777216; + + return current; + } + + private static long TwoThousandthEvolution(long n) { + for (var i = 0; i < 2000; ++i) + n = NextSecretNumber(n); + + return n; + } + + protected override long SolvePartOne() => Shared.Split(Input).Select(long.Parse).Sum(TwoThousandthEvolution); + + protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + private const string? ExampleInput = + """ + 1 + 10 + 100 + 2024 + """; + + [Fact] + public void CheckBitShifts() { + Assert.Equal(64, 1 << 6); + Assert.Equal(1, 32 >> 5); + Assert.Equal(2048, 1 << 11); + } + + [Fact] + public void CheckXorOperation() { + Assert.Equal(37, 42 ^ 15); + } + + [Fact] + public void CheckMixOperation() { + Assert.Equal(16113920, 100000000 % 16777216); + } + + [Fact] + public void SolvesNextTenSecretNumbers() { + var current = 123L; + + int[] expected = [ + 15887950, + 16495136, + 527345, + 704524, + 1553684, + 12683156, + 11100544, + 12249484, + 7753432, + 5908254 + ]; + + for (var i = 0; i < 10; ++i) { + Trace.WriteLine((current = NextSecretNumber(current)).ToString()); + Assert.Equal(expected[i], current); + } + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day22(ExampleInput, Output).SolvePartOne(); + Assert.Equal(37327623, actual); + } +} \ No newline at end of file From 74c97f766c1f0675506fab4143d9fbd96d3ca89d Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sun, 22 Dec 2024 14:23:27 +0000 Subject: [PATCH 37/43] 2024 day 22 part 2 --- AdventOfCode.CSharp/2024/Day22.cs | 89 ++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/AdventOfCode.CSharp/2024/Day22.cs b/AdventOfCode.CSharp/2024/Day22.cs index 0256350..62f11c4 100644 --- a/AdventOfCode.CSharp/2024/Day22.cs +++ b/AdventOfCode.CSharp/2024/Day22.cs @@ -30,7 +30,45 @@ private static long TwoThousandthEvolution(long n) { protected override long SolvePartOne() => Shared.Split(Input).Select(long.Parse).Sum(TwoThousandthEvolution); - protected override long SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + private static IEnumerable Prices(long n) { + for (var i = 0; i < 2001; ++i) { + yield return (int)(n % 10); + n = NextSecretNumber(n); + } + } + + private static (int price, int change) Change(Tuple arg) => (arg.Item2, arg.Item2 - arg.Item1); + + private static ((int, int, int, int) changes, int price)[] Windows((int price, int change)[] seq) { + var r = new ((int, int, int, int) changes, int price)[seq.Length - 3]; + + for (int i = 3, c = seq.Length; i < c; ++i) + r[i - 3] = ((seq[i - 3].change, seq[i - 2].change, seq[i - 1].change, seq[i].change), seq[i].price); + + return r; + } + + protected override long SolvePartTwo() { + // parse input to price/change lists + var monkeys = + Shared.Split(Input) + .Select(s => Prices(long.Parse(s)).Pairwise().Select(Change).ToArray()); + + // determine the individual sequence->price dictionaries + var sellingPrices = monkeys.Select(pairs => + Windows(pairs) + .Where(p => p.price > 0) // we only care if we can get a half-decent price + .GroupBy(p => p.changes) + .ToDictionary(p => p.Key, p => p.First().price)).ToArray(); + + var allSequences = sellingPrices + .SelectMany(d => d.Keys) + .Distinct() + .ToArray(); + + return allSequences.Max(seq => + sellingPrices.Sum(sp => sp.GetValueOrDefault(seq, 0))); + } private const string? ExampleInput = """ @@ -39,6 +77,14 @@ private static long TwoThousandthEvolution(long n) { 100 2024 """; + + private const string? ExampleInputPartTwo = + """ + 1 + 2 + 3 + 2024 + """; [Fact] public void CheckBitShifts() { @@ -80,9 +126,50 @@ public void SolvesNextTenSecretNumbers() { } } + [Fact] + public void CalculatesFirstTenPricesCorrectly() { + int[] expected = [ + 3, + 0, + 6, + 5, + 4, + 4, + 6, + 4, + 4, + 2 + ]; + + Assert.Equal(expected, Prices(123L).Take(10)); + } + + [Fact] + public void CalculatesFirstNineChangesCorrectly() { + (int,int)[] expected = [ + (0, -3), + (6, 6), + (5, -1), + (4, -1), + (4, 0), + (6, 2), + (4, -2), + (4, 0), + (2, -2) + ]; + + Assert.Equal(expected, Prices(123L).Take(10).Pairwise().Select(Change)); + } + [Fact] public void SolvesPartOneExample() { var actual = new Day22(ExampleInput, Output).SolvePartOne(); Assert.Equal(37327623, actual); } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day22(ExampleInputPartTwo, Output).SolvePartTwo(); + Assert.Equal(23, actual); + } } \ No newline at end of file From 68cd6fbfae16f6ad0b5eeae9458c73f97fad1725 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Sun, 22 Dec 2024 14:45:34 +0000 Subject: [PATCH 38/43] 2024 day 22 part 2 (faster) --- AdventOfCode.CSharp/2024/Day22.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day22.cs b/AdventOfCode.CSharp/2024/Day22.cs index 62f11c4..16a9801 100644 --- a/AdventOfCode.CSharp/2024/Day22.cs +++ b/AdventOfCode.CSharp/2024/Day22.cs @@ -1,3 +1,4 @@ +using System.Collections.Frozen; using System.Runtime.CompilerServices; using AdventOfCode.Core; using Xunit; @@ -39,12 +40,18 @@ private static IEnumerable Prices(long n) { private static (int price, int change) Change(Tuple arg) => (arg.Item2, arg.Item2 - arg.Item1); - private static ((int, int, int, int) changes, int price)[] Windows((int price, int change)[] seq) { - var r = new ((int, int, int, int) changes, int price)[seq.Length - 3]; - - for (int i = 3, c = seq.Length; i < c; ++i) - r[i - 3] = ((seq[i - 3].change, seq[i - 2].change, seq[i - 1].change, seq[i].change), seq[i].price); - + private static (int changeKey, int price)[] Windows((int price, int change)[] seq) { + var r = new (int, int)[seq.Length - 3]; + + for (int i = 3, c = seq.Length; i < c; ++i) { + var changeKey1 = 10 + seq[i - 3].change; + var changeKey2 = 10 + seq[i - 2].change; + var changeKey3 = 10 + seq[i - 1].change; + var changeKey4 = 10 + seq[i].change; + var changeKey = changeKey1 * 1000000 + changeKey2 * 10000 + changeKey3 * 100 + changeKey4; + r[i - 3] = (changeKey, seq[i].price); + } + return r; } @@ -58,8 +65,8 @@ protected override long SolvePartTwo() { var sellingPrices = monkeys.Select(pairs => Windows(pairs) .Where(p => p.price > 0) // we only care if we can get a half-decent price - .GroupBy(p => p.changes) - .ToDictionary(p => p.Key, p => p.First().price)).ToArray(); + .GroupBy(p => p.changeKey) + .ToFrozenDictionary(p => p.Key, p => p.First().price)).ToArray(); var allSequences = sellingPrices .SelectMany(d => d.Keys) From c78011530890b51f0daec751ee1e7be18a575b44 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Mon, 23 Dec 2024 08:21:19 +0000 Subject: [PATCH 39/43] 2024 day 23 part 1 --- AdventOfCode.CSharp/2024/Day23.cs | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day23.cs diff --git a/AdventOfCode.CSharp/2024/Day23.cs b/AdventOfCode.CSharp/2024/Day23.cs new file mode 100644 index 0000000..d44496d --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day23.cs @@ -0,0 +1,97 @@ +using System.Collections.Frozen; +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day23(string? input = null, ITestOutputHelper? outputHelper = null) + : Solver(input, outputHelper) { + private (string left, string right)[] Parse(string input) => + Shared.Split(input).Select(s => { + var parts = s.Split("-"); + return (parts[0], parts[1]); + }).ToArray(); + + private FrozenDictionary> GetConnections() { + var connections = new Dictionary>(); + + foreach (var connection in Parse(Input)) { + if (!connections.TryGetValue(connection.left, out var left)) + connections[connection.left] = [connection.right]; + else + left.Add(connection.right); + + if (!connections.TryGetValue(connection.right, out var right)) + connections[connection.right] = [connection.left]; + else + right.Add(connection.left); + } + + return connections + .ToDictionary(p => p.Key, p => p.Value.ToFrozenSet()) + .ToFrozenDictionary(); + } + + protected override string SolvePartOne() { + var connections = GetConnections(); + var triplets = new HashSet(); + + foreach (var connection in connections.Where(p => p.Key.StartsWith('t'))) { + var node = connection.Value.ToArray(); + + for (var i = 0; i < node.Length; ++i) { + for (var j = i + 1; j < node.Length; ++j) { + if (connections[node[i]].Contains(node[j])) + triplets.Add(string.Join(',', new[] { connection.Key, node[i], node[j] }.OrderBy(v => v))); + } + } + } + + return triplets.Count.ToString(); + } + + protected override string SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + private const string? ExampleInput = + """ + kh-tc + qp-kh + de-cg + ka-co + yn-aq + qp-ub + cg-tb + vc-aq + tb-ka + wh-tc + yn-cg + kh-ub + ta-co + de-co + tc-td + tb-wq + wh-td + ta-ka + td-qp + aq-cg + wq-ub + ub-vc + de-ta + wq-aq + wq-vc + wh-yn + ka-de + kh-ta + co-tc + wh-qp + tb-vc + td-yn + """; + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day23(ExampleInput, Output).SolvePartOne(); + Assert.Equal("7", actual); + } +} \ No newline at end of file From 95223b6b1a234c60de8295cd201b1738788fcb37 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Mon, 23 Dec 2024 09:09:38 +0000 Subject: [PATCH 40/43] 2024 day 23 part 2 --- AdventOfCode.CSharp/2024/Day23.cs | 48 +++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day23.cs b/AdventOfCode.CSharp/2024/Day23.cs index d44496d..79a340a 100644 --- a/AdventOfCode.CSharp/2024/Day23.cs +++ b/AdventOfCode.CSharp/2024/Day23.cs @@ -29,7 +29,7 @@ private FrozenDictionary> GetConnections() { } return connections - .ToDictionary(p => p.Key, p => p.Value.ToFrozenSet()) + .ToDictionary(p => p.Key, p => p.Value.OrderBy(v => v).ToFrozenSet()) .ToFrozenDictionary(); } @@ -51,7 +51,45 @@ protected override string SolvePartOne() { return triplets.Count.ToString(); } - protected override string SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + protected override string SolvePartTwo() { + var connections = GetConnections(); + var i = 0; + var longestSet = string.Empty; + + var seen = new HashSet(); + var queue = new Queue(Shared.Split(Input).Select(s => s.Split('-'))); + + while (queue.TryDequeue(out var workingSet)) { + // calculate the set key + Array.Sort(workingSet); + var key = string.Join(',', workingSet); + + // ensure we are novel + if (!seen.Add(key)) + continue; + + // little progress report + if (++i % 10000 == 0) + Output.WriteLine($"i = {i}, queue = {queue.Count}, current = {key}"); + + // calculate the options for the next step + var remainingConnections = connections[workingSet[0]].Except(workingSet); + var didQueue = false; + + foreach (var nextNode in remainingConnections) { + if (connections[nextNode].IsSupersetOf(workingSet)) { + queue.Enqueue(workingSet.Concat([nextNode]).ToArray()); + didQueue = true; + } + } + + // if we couldn't get any bigger, see if we were the biggest so far + if (!didQueue && key.Length > longestSet.Length) + longestSet = key; + } + + return longestSet; + } private const string? ExampleInput = """ @@ -94,4 +132,10 @@ public void SolvesPartOneExample() { var actual = new Day23(ExampleInput, Output).SolvePartOne(); Assert.Equal("7", actual); } + + [Fact] + public void SolvesPartTwoExample() { + var actual = new Day23(ExampleInput, Output).SolvePartTwo(); + Assert.Equal("co,de,ka,ta", actual); + } } \ No newline at end of file From ae451df2f4196e997e2686c2bf8aa19740097667 Mon Sep 17 00:00:00 2001 From: James Holwell Date: Mon, 23 Dec 2024 09:56:25 +0000 Subject: [PATCH 41/43] 2024 day 23 part 2 (faster) --- AdventOfCode.CSharp/2024/Day23.cs | 87 +++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/AdventOfCode.CSharp/2024/Day23.cs b/AdventOfCode.CSharp/2024/Day23.cs index 79a340a..22eb7a1 100644 --- a/AdventOfCode.CSharp/2024/Day23.cs +++ b/AdventOfCode.CSharp/2024/Day23.cs @@ -35,11 +35,16 @@ private FrozenDictionary> GetConnections() { protected override string SolvePartOne() { var connections = GetConnections(); + + return GetTriplets(connections).Count.ToString(); + } + + private static HashSet GetTriplets(FrozenDictionary> connections) { var triplets = new HashSet(); foreach (var connection in connections.Where(p => p.Key.StartsWith('t'))) { var node = connection.Value.ToArray(); - + for (var i = 0; i < node.Length; ++i) { for (var j = i + 1; j < node.Length; ++j) { if (connections[node[i]].Contains(node[j])) @@ -48,50 +53,76 @@ protected override string SolvePartOne() { } } - return triplets.Count.ToString(); + return triplets; } protected override string SolvePartTwo() { var connections = GetConnections(); - var i = 0; - var longestSet = string.Empty; + + // hey, we already did some of the work in part 1! + var seeds = GetTriplets(connections) + .Select(t => (key: t, working: t.Split(','))) + .Select(i => ( + i.key, + i.working, + remaining: connections[i.working[0]].Except(i.working).ToArray())); var seen = new HashSet(); - var queue = new Queue(Shared.Split(Input).Select(s => s.Split('-'))); - - while (queue.TryDequeue(out var workingSet)) { - // calculate the set key - Array.Sort(workingSet); - var key = string.Join(',', workingSet); - - // ensure we are novel - if (!seen.Add(key)) - continue; - - // little progress report - if (++i % 10000 == 0) - Output.WriteLine($"i = {i}, queue = {queue.Count}, current = {key}"); + var queue = new Queue<(string key, string[] working, string[] remaining)>(seeds); + var longestSet = string.Empty; + while (queue.TryDequeue(out var item)) { // calculate the options for the next step - var remainingConnections = connections[workingSet[0]].Except(workingSet); var didQueue = false; - foreach (var nextNode in remainingConnections) { - if (connections[nextNode].IsSupersetOf(workingSet)) { - queue.Enqueue(workingSet.Concat([nextNode]).ToArray()); - didQueue = true; - } + foreach (var nextNode in item.remaining) { + if (!connections[nextNode].IsSupersetOf(item.working)) + continue; + + /* + * The following is equivalent to + * + * var nextWorking = item.working.Concat([nextNode]).ToArray(); + * Array.Sort(nextWorking); + * + * var nextKey = string.Join(',', nextWorking); + * + * But leverages the already-ordered nature of the working set + * + */ + + // fast initialise next working set + var nextWorking = new string[item.working.Length + 1]; + var place = 0; + while (place < item.working.Length && string.Compare(item.working[place], nextNode, StringComparison.Ordinal) < 0) + ++place; + Array.Copy(item.working, nextWorking, place); + nextWorking[place] = nextNode; + if (place <= item.working.Length) + Array.Copy(item.working, place, nextWorking, place + 1, item.working.Length - place); + + // fast initialise next key + var nextKey = + place == item.working.Length + ? $"{item.key},{nextNode}" + : $"{item.key[..(place * 3)]}{nextNode},{item.key[(place * 3)..]}"; + + // test if we've already seen this set + if (seen.Add(nextKey)) + queue.Enqueue((nextKey, nextWorking, item.remaining[1..])); + + didQueue = true; } // if we couldn't get any bigger, see if we were the biggest so far - if (!didQueue && key.Length > longestSet.Length) - longestSet = key; + if (!didQueue && item.key.Length > longestSet.Length) + longestSet = item.key; } return longestSet; } - private const string? ExampleInput = + private const string? ExampleInput = """ kh-tc qp-kh @@ -132,7 +163,7 @@ public void SolvesPartOneExample() { var actual = new Day23(ExampleInput, Output).SolvePartOne(); Assert.Equal("7", actual); } - + [Fact] public void SolvesPartTwoExample() { var actual = new Day23(ExampleInput, Output).SolvePartTwo(); From 77452032ecd3469df69ae325e123dacedd626b3b Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 24 Dec 2024 07:57:08 +0000 Subject: [PATCH 42/43] 2024 day 24 part 1 --- AdventOfCode.CSharp/2024/Day24.cs | 178 ++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 AdventOfCode.CSharp/2024/Day24.cs diff --git a/AdventOfCode.CSharp/2024/Day24.cs b/AdventOfCode.CSharp/2024/Day24.cs new file mode 100644 index 0000000..c9354e5 --- /dev/null +++ b/AdventOfCode.CSharp/2024/Day24.cs @@ -0,0 +1,178 @@ +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.CSharp._2024; + +public class Day24(string? input = null, ITestOutputHelper? outputHelper = null) + : Solver(input, outputHelper) { + private Dictionary state = new(); + + private ((string wire, bool state)[] assignments, (string operand, string left, string right, string output)[] gates) Parse() { + var parts = Input.SplitBy("\n\n"); + + var assignments = parts[0].Split('\n') + .Select(s => s.Split(": ")) + .Select(ss => (wire: ss[0], state: (int?)int.Parse(ss[1]) == 1)) + .ToArray(); + + var gates = parts[1].Split('\n') + .Select(s => s.Split(' ', 5)) + .Select(ss => (operand: ss[1], left: ss[0], right: ss[2], output: ss[4])) + .ToArray(); + + return (assignments, gates); + } + + protected override string SolvePartOne() { + var (assignments, gates) = Parse(); + + state = assignments.ToDictionary(a => a.wire, a => a.state); + + var queue = new Queue<(string operand, string left, string right, string output)>(gates); + while (queue.TryDequeue(out var gate)) { + if (!state.TryGetValue(gate.left, out var left) + || !state.TryGetValue(gate.right, out var right)) { + queue.Enqueue(gate); + continue; + } + + state[gate.output] = gate.operand switch { + "AND" => left && right, + "OR" => left || right, + "XOR" => left ^ right, + _ => throw new InvalidOperationException($"Unknown operand: {gate.operand}") + }; + } + + foreach (var wire in state.OrderBy(p => p.Key)) + Trace.WriteLine($"{wire.Key}: {(wire.Value ? "1" : "0")}"); + + var z = state.Where(wire => wire.Key.StartsWith('z') && wire.Value) + .Aggregate(0L, (acc, i) => acc + (1L << int.Parse(i.Key[1..]))); + + return z.ToString(); + } + + protected override string SolvePartTwo() => throw new NotImplementedException("Solve part 1 first"); + + private const string? SmallExampleInput = + """ + x00: 1 + x01: 1 + x02: 1 + y00: 0 + y01: 1 + y02: 0 + + x00 AND y00 -> z00 + x01 XOR y01 -> z01 + x02 OR y02 -> z02 + """; + + private const string? ExampleInput = + """ + x00: 1 + x01: 0 + x02: 1 + x03: 1 + x04: 0 + y00: 1 + y01: 1 + y02: 1 + y03: 1 + y04: 1 + + ntg XOR fgs -> mjb + y02 OR x01 -> tnw + kwq OR kpj -> z05 + x00 OR x03 -> fst + tgd XOR rvg -> z01 + vdt OR tnw -> bfw + bfw AND frj -> z10 + ffh OR nrd -> bqk + y00 AND y03 -> djm + y03 OR y00 -> psh + bqk OR frj -> z08 + tnw OR fst -> frj + gnj AND tgd -> z11 + bfw XOR mjb -> z00 + x03 OR x00 -> vdt + gnj AND wpb -> z02 + x04 AND y00 -> kjc + djm OR pbm -> qhw + nrd AND vdt -> hwm + kjc AND fst -> rvg + y04 OR y02 -> fgs + y01 AND x02 -> pbm + ntg OR kjc -> kwq + psh XOR fgs -> tgd + qhw XOR tgd -> z09 + pbm OR djm -> kpj + x03 XOR y03 -> ffh + x00 XOR y04 -> ntg + bfw OR bqk -> z06 + nrd XOR fgs -> wpb + frj XOR qhw -> z04 + bqk OR frj -> z07 + y03 OR x01 -> nrd + hwm AND bqk -> z03 + tgd XOR rvg -> z12 + tnw OR pbm -> gnj + """; + + [Fact] + public void SolvesPartSmallExample() { + var actual = new Day24(SmallExampleInput, Output).SolvePartOne(); + Assert.Equal("4", actual); + } + + [Fact] + public void VerifyPartOneRegisters() { + var solver = new Day24(ExampleInput, Output); + solver.SolvePartOne(); + + Assert.True(solver.state["bfw"]); // bfw: 1 + Assert.True(solver.state["bqk"]); // bqk: 1 + Assert.True(solver.state["djm"]); // djm: 1 + Assert.False(solver.state["ffh"]); // ffh: 0 + Assert.True(solver.state["fgs"]); // fgs: 1 + Assert.True(solver.state["frj"]); // frj: 1 + Assert.True(solver.state["fst"]); // fst: 1 + Assert.True(solver.state["gnj"]); // gnj: 1 + Assert.True(solver.state["hwm"]); // hwm: 1 + Assert.False(solver.state["kjc"]); // kjc: 0 + Assert.True(solver.state["kpj"]); // kpj: 1 + Assert.False(solver.state["kwq"]); // kwq: 0 + Assert.True(solver.state["mjb"]); // mjb: 1 + Assert.True(solver.state["nrd"]); // nrd: 1 + Assert.False(solver.state["ntg"]); // ntg: 0 + Assert.True(solver.state["pbm"]); // pbm: 1 + Assert.True(solver.state["psh"]); // psh: 1 + Assert.True(solver.state["qhw"]); // qhw: 1 + Assert.False(solver.state["rvg"]); // rvg: 0 + Assert.False(solver.state["tgd"]); // tgd: 0 + Assert.True(solver.state["tnw"]); // tnw: 1 + Assert.True(solver.state["vdt"]); // vdt: 1 + Assert.False(solver.state["wpb"]); // wpb: 0 + Assert.False(solver.state["z00"]); // z00: 0 + Assert.False(solver.state["z01"]); // z01: 0 + Assert.False(solver.state["z02"]); // z02: 0 + Assert.True(solver.state["z03"]); // z03: 1 + Assert.False(solver.state["z04"]); // z04: 0 + Assert.True(solver.state["z05"]); // z05: 1 + Assert.True(solver.state["z06"]); // z06: 1 + Assert.True(solver.state["z07"]); // z07: 1 + Assert.True(solver.state["z08"]); // z08: 1 + Assert.True(solver.state["z09"]); // z09: 1 + Assert.True(solver.state["z10"]); // z10: 1 + Assert.False(solver.state["z11"]); // z11: 0 + Assert.False(solver.state["z12"]); // z12: 0 + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day24(ExampleInput, Output).SolvePartOne(); + Assert.Equal("2024", actual); + } +} \ No newline at end of file From 55f043f1fd6063a9b737d229ac6bc550d7d9617b Mon Sep 17 00:00:00 2001 From: James Holwell Date: Tue, 24 Dec 2024 09:43:41 +0000 Subject: [PATCH 43/43] 2024 day 24 part 2 (wip) --- 2024-day24.html | 936 +++++++++++++++++++++++++++++++ AdventOfCode.Today/2024/Day24.cs | 232 ++++++++ 2 files changed, 1168 insertions(+) create mode 100644 2024-day24.html create mode 100644 AdventOfCode.Today/2024/Day24.cs diff --git a/2024-day24.html b/2024-day24.html new file mode 100644 index 0000000..2615456 --- /dev/null +++ b/2024-day24.html @@ -0,0 +1,936 @@ + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/AdventOfCode.Today/2024/Day24.cs b/AdventOfCode.Today/2024/Day24.cs new file mode 100644 index 0000000..fd914fa --- /dev/null +++ b/AdventOfCode.Today/2024/Day24.cs @@ -0,0 +1,232 @@ +using System.Text; +using AdventOfCode.Core; +using Xunit; +using Xunit.Abstractions; + +namespace AdventOfCode.Today._2024; + +public class Day24(string? input = null, ITestOutputHelper? outputHelper = null) + : Solver(input, outputHelper) { + private Dictionary state = new(); + + private ((string wire, bool state)[] assignments, (string operand, string left, string right, string output)[] gates) Parse() { + var parts = Input.SplitBy("\n\n"); + + var assignments = parts[0].Split('\n') + .Select(s => s.Split(": ")) + .Select(ss => (wire: ss[0], state: (int?)int.Parse(ss[1]) == 1)) + .ToArray(); + + var gates = parts[1].Split('\n') + .Select(s => s.Split(' ', 5)) + .Select(ss => (operand: ss[1], left: ss[0], right: ss[2], output: ss[4])) + .ToArray(); + + return (assignments, gates); + } + + protected override string SolvePartOne() { + var (assignments, gates) = Parse(); + + state = assignments.ToDictionary(a => a.wire, a => a.state); + + var queue = new Queue<(string operand, string left, string right, string output)>(gates); + while (queue.TryDequeue(out var gate)) { + if (!state.TryGetValue(gate.left, out var left) + || !state.TryGetValue(gate.right, out var right)) { + queue.Enqueue(gate); + continue; + } + + state[gate.output] = gate.operand switch { + "AND" => left && right, + "OR" => left || right, + "XOR" => left ^ right, + _ => throw new InvalidOperationException($"Unknown operand: {gate.operand}") + }; + } + + foreach (var wire in state.OrderBy(p => p.Key)) + Trace.WriteLine($"{wire.Key}: {(wire.Value ? "1" : "0")}"); + + var z = state.Where(wire => wire.Key.StartsWith('z') && wire.Value) + .Aggregate(0L, (acc, i) => acc + (1L << int.Parse(i.Key[1..]))); + + return z.ToString(); + } + + protected override string SolvePartTwo() { + var (assignments, gates) = Parse(); + + OutputD3(assignments, gates); + + throw new NotImplementedException("Solve part 1 first"); + } + + private void OutputD3((string wire, bool state)[] assignments, (string operand, string left, string right, string output)[] gates) { + var sb = new StringBuilder(4096); + sb.AppendLine("var data = {"); + + // output all nodes + sb.AppendLine("nodes : ["); + HashSet nodes = []; + foreach (var wire in gates) { + nodes.Add(wire.left); + nodes.Add(wire.right); + nodes.Add(wire.output); + } + + // make operator list + var labels = gates.ToDictionary(g => g.output, g => g.operand); + + foreach (var node in nodes.Where(n => n.StartsWith('x')).OrderBy(w => w)) + sb.AppendLine($$"""{ id: "{{node}}", _x: "-800", _y: "{{-500 + 50 * int.Parse(node[1..])}}" },"""); + + foreach (var node in nodes.Where(n => n.StartsWith('y')).OrderBy(w => w)) + sb.AppendLine($$"""{ id: "{{node}}", _x: "-790", _y: "{{-490 + 50 * int.Parse(node[1..])}}" },"""); + + foreach (var node in nodes.Where(n => n.StartsWith('z')).OrderBy(w => w)) + sb.AppendLine($$"""{ id: "{{node}}", _x: "800", _y: "{{-470 + 50 * int.Parse(node[1..])}}" },"""); + + foreach (var node in nodes.Where(n => !n.StartsWith('x') && !n.StartsWith('y') && !n.StartsWith('z')).OrderBy(w => w)) + sb.AppendLine($$"""{ id: "{{node}}", type: "{{(labels.TryGetValue(node, out var value) ? value : node)}}" },"""); + + sb.AppendLine("],"); + sb.AppendLine("links: ["); + + foreach (var gate in gates.OrderBy(g => g.output)) { + sb.AppendLine($$"""{source: "{{gate.left}}", target: "{{gate.output}}" },"""); + sb.AppendLine($$"""{source: "{{gate.right}}", target: "{{gate.output}}" },"""); + } + + sb.AppendLine("]"); + sb.AppendLine("}"); + + Output.WriteLine(sb.ToString()); + } + + private const string? SmallExampleInput = + """ + x00: 1 + x01: 1 + x02: 1 + y00: 0 + y01: 1 + y02: 0 + + x00 AND y00 -> z00 + x01 XOR y01 -> z01 + x02 OR y02 -> z02 + """; + + private const string? ExampleInput = + """ + x00: 1 + x01: 0 + x02: 1 + x03: 1 + x04: 0 + y00: 1 + y01: 1 + y02: 1 + y03: 1 + y04: 1 + + ntg XOR fgs -> mjb + y02 OR x01 -> tnw + kwq OR kpj -> z05 + x00 OR x03 -> fst + tgd XOR rvg -> z01 + vdt OR tnw -> bfw + bfw AND frj -> z10 + ffh OR nrd -> bqk + y00 AND y03 -> djm + y03 OR y00 -> psh + bqk OR frj -> z08 + tnw OR fst -> frj + gnj AND tgd -> z11 + bfw XOR mjb -> z00 + x03 OR x00 -> vdt + gnj AND wpb -> z02 + x04 AND y00 -> kjc + djm OR pbm -> qhw + nrd AND vdt -> hwm + kjc AND fst -> rvg + y04 OR y02 -> fgs + y01 AND x02 -> pbm + ntg OR kjc -> kwq + psh XOR fgs -> tgd + qhw XOR tgd -> z09 + pbm OR djm -> kpj + x03 XOR y03 -> ffh + x00 XOR y04 -> ntg + bfw OR bqk -> z06 + nrd XOR fgs -> wpb + frj XOR qhw -> z04 + bqk OR frj -> z07 + y03 OR x01 -> nrd + hwm AND bqk -> z03 + tgd XOR rvg -> z12 + tnw OR pbm -> gnj + """; + + [Fact] + public void SolvesPartSmallExample() { + var actual = new Day24(SmallExampleInput, Output).SolvePartOne(); + Assert.Equal("4", actual); + } + + [Fact] + public void VerifyPartOneRegisters() { + var solver = new Day24(ExampleInput, Output); + solver.SolvePartOne(); + + Assert.True(solver.state["bfw"]); // bfw: 1 + Assert.True(solver.state["bqk"]); // bqk: 1 + Assert.True(solver.state["djm"]); // djm: 1 + Assert.False(solver.state["ffh"]); // ffh: 0 + Assert.True(solver.state["fgs"]); // fgs: 1 + Assert.True(solver.state["frj"]); // frj: 1 + Assert.True(solver.state["fst"]); // fst: 1 + Assert.True(solver.state["gnj"]); // gnj: 1 + Assert.True(solver.state["hwm"]); // hwm: 1 + Assert.False(solver.state["kjc"]); // kjc: 0 + Assert.True(solver.state["kpj"]); // kpj: 1 + Assert.False(solver.state["kwq"]); // kwq: 0 + Assert.True(solver.state["mjb"]); // mjb: 1 + Assert.True(solver.state["nrd"]); // nrd: 1 + Assert.False(solver.state["ntg"]); // ntg: 0 + Assert.True(solver.state["pbm"]); // pbm: 1 + Assert.True(solver.state["psh"]); // psh: 1 + Assert.True(solver.state["qhw"]); // qhw: 1 + Assert.False(solver.state["rvg"]); // rvg: 0 + Assert.False(solver.state["tgd"]); // tgd: 0 + Assert.True(solver.state["tnw"]); // tnw: 1 + Assert.True(solver.state["vdt"]); // vdt: 1 + Assert.False(solver.state["wpb"]); // wpb: 0 + Assert.False(solver.state["z00"]); // z00: 0 + Assert.False(solver.state["z01"]); // z01: 0 + Assert.False(solver.state["z02"]); // z02: 0 + Assert.True(solver.state["z03"]); // z03: 1 + Assert.False(solver.state["z04"]); // z04: 0 + Assert.True(solver.state["z05"]); // z05: 1 + Assert.True(solver.state["z06"]); // z06: 1 + Assert.True(solver.state["z07"]); // z07: 1 + Assert.True(solver.state["z08"]); // z08: 1 + Assert.True(solver.state["z09"]); // z09: 1 + Assert.True(solver.state["z10"]); // z10: 1 + Assert.False(solver.state["z11"]); // z11: 0 + Assert.False(solver.state["z12"]); // z12: 0 + } + + [Fact] + public void SolvesPartOneExample() { + var actual = new Day24(ExampleInput, Output).SolvePartOne(); + Assert.Equal("2024", actual); + } + + [Fact] + public void SolvesPartTwoExample() { + new Day24(ExampleInput, Output).SolvePartTwo(); + } +} \ No newline at end of file