Software developer in Halifax, Nova Scotia, Canada. Dad to 2 kids.

Find me on the Fediverse in other places:

  • 2 Posts
  • 148 Comments
Joined 2 years ago
cake
Cake day: June 6th, 2023

help-circle
  • C#

    Part 2 was pretty much the same as Part 2 except we can’t short-circuit when we find the first match. So, implement a cache of each sub-pattern and the number of ways to form it from the towels, and things get much faster.

    using System.Collections.Immutable;
    using System.Diagnostics;
    using Common;
    
    namespace Day19;
    
    static class Program
    {
        static void Main()
        {
            var start = Stopwatch.GetTimestamp();
    
            var sampleInput = ReceiveInput("sample.txt");
            var programInput = ReceiveInput("input.txt");
    
            Console.WriteLine($"Part 1 sample: {Part1(sampleInput)}");
            Console.WriteLine($"Part 1 input: {Part1(programInput)}");
    
            Console.WriteLine($"Part 2 sample: {Part2(sampleInput)}");
            Console.WriteLine($"Part 2 input: {Part2(programInput)}");
    
            Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
        }
    
        static object Part1(Input input)
        {
            return input.Patterns
                .Select(p => AnyTowelMatches(p, input.Towels) ? 1 : 0)
                .Sum();
        }
    
        static object Part2(Input input)
        {
            var matchCache = new Dictionary<string, long>();
            return input.Patterns
                .Select(p => CountTowelMatches(p, input.Towels, matchCache))
                .Sum();
        }
    
        private static bool AnyTowelMatches(
            string pattern,
            ImmutableArray<string> towels)
        {
            return towels
                .Where(t => t.Length <= pattern.Length)
                .Select(t =>
                    !pattern.StartsWith(t) ? false :
                    (pattern.Length == t.Length) ? true :
                    AnyTowelMatches(pattern.Substring(t.Length), towels))
                .Any(r => r);
        }
    
        private static long CountTowelMatches(
            string pattern,
            ImmutableArray<string> towels,
            Dictionary<string, long> matchCache)
        {
            if (matchCache.TryGetValue(pattern, out var count)) return count;
    
            count = towels
                .Where(t => t.Length <= pattern.Length)
                .Select(t => 
                    !pattern.StartsWith(t) ? 0 :
                    (pattern.Length == t.Length) ? 1 :
                    CountTowelMatches(pattern.Substring(t.Length), towels, matchCache))
                .Sum();
    
            matchCache[pattern] = count;
            return count;
        }
    
        static Input ReceiveInput(string file)
        {
            using var reader = new StreamReader(file);
    
            var towels = reader.ReadLine()!.SplitAndTrim(',').ToImmutableArray();
            var patterns = new List<string>();
            reader.ReadLine();
            var line = reader.ReadLine();
            while (line is not null)
            {
                patterns.Add(line);
                line = reader.ReadLine();
            }
    
            return new Input()
            {
                Towels = towels,
                Patterns = [..patterns],
            };
        }
    
        public class Input
        {
            public required ImmutableArray<string> Towels { get; init; }
            public required ImmutableArray<string> Patterns { get; init; }
        }
    }
    

  • C#

    Part 1 was straight forward Dykstra with a cost of 1 for each move. Part 2 was a binary search from the number of corrupted bytes given to us for Part 1 (where we know a path can be found) to the total number of corrupted bytes.

    using System.Collections.Immutable;
    using System.Diagnostics;
    using Common;
    
    namespace Day18;
    
    static class Program
    {
        static void Main()
        {
            var start = Stopwatch.GetTimestamp();
    
            var sampleInput = ReceiveInput("sample.txt");
            var sampleBounds = new Point(7,7);
            
            var programInput = ReceiveInput("input.txt");
            var programBounds = new Point(71, 71);
    
            Console.WriteLine($"Part 1 sample: {Part1(sampleInput, 12, sampleBounds)}");
            Console.WriteLine($"Part 1 input: {Part1(programInput, 1024, programBounds)}");
    
            Console.WriteLine($"Part 2 sample: {Part2(sampleInput, 12, sampleBounds)}");
            Console.WriteLine($"Part 2 input: {Part2(programInput, 1024, programBounds)}");
    
            Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
        }
    
        static int Part1(ImmutableArray<Point> input, int num, Point bounds) => FindBestPath(
            new Point(0, 0),
            new Point(bounds.Row - 1, bounds.Col - 1),
            input.Take(num).ToImmutableHashSet(),
            bounds);
    
        static object Part2(ImmutableArray<Point> input, int num, Point bounds)
        {
            var start = num;
            var end = input.Length;
            
            while (start != end)
            {
                var check = (start + end) / 2;
                if (Part1(input, check, bounds) < 0) end = check;
                else start = check + 1;
            }
    
            var lastPoint = input[start - 1];
            return $"{lastPoint.Col},{lastPoint.Row}";
        }
    
        record struct State(Point Location, int Steps);
    
        static int FindBestPath(Point start, Point end, ISet<Point> corruptedBytes, Point bounds)
        {
            var seenStates = new Dictionary<Point, int>();
    
            var queue = new Queue<State>();
            queue.Enqueue(new State(start, 0));
            while (queue.TryDequeue(out var state))
            {
                if (state.Location == end) return state.Steps;
                
                if (seenStates.TryGetValue(state.Location, out var bestSteps))
                {
                    if (state.Steps >= bestSteps) continue;
                }
                
                seenStates[state.Location] = state.Steps;
                queue.EnqueueRange(state.Location.GetCardinalMoves()
                    .Where(p => p.IsInBounds(bounds) && !corruptedBytes.Contains(p))
                    .Select(p => new State(p, state.Steps + 1)));
            }
    
            return -1;
        }
    
        static ImmutableArray<Point> ReceiveInput(string file) => File.ReadAllLines(file)
            .Select(l => l.Split(','))
            .Select(p => new Point(int.Parse(p[1]), int.Parse(p[0])))
            .ToImmutableArray();
    }
    

  • C#

    This one is mostly thanks to reading @mykl@[email protected]’s code to understand WTF was going on for part 2, and then once I understood the basics, finally got to solving it myself. Instructions were read in as long because I didn’t want to deal with int vs. long all the time.

    using System.Collections.Immutable;
    using System.Diagnostics;
    using Common;
    
    namespace Day17;
    
    static class Program
    {
        public record State(long A, long B, long C, int InstPtr, ImmutableList<long> Output);
    
        static void Main()
        {
            var start = Stopwatch.GetTimestamp();
    
            var (sampleReg, sampleInst) = ReceiveInput("sample.txt");
            var (inputReg, inputInst) = ReceiveInput("input.txt");
    
            Console.WriteLine($"Part 1 sample: {Part1(sampleReg, sampleInst)}");
            Console.WriteLine($"Part 1 input: {Part1(inputReg, inputInst)}");
    
            (sampleReg, sampleInst) = ReceiveInput("sample2.txt");
            Console.WriteLine($"Part 2 sample: {Part2(sampleReg, sampleInst)}");
            Console.WriteLine($"Part 2 input: {Part2(inputReg, inputInst)}");
    
            Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
        }
    
        static object Part1(State state, ImmutableArray<long> instructions) =>
            Execute(instructions, state).Output.StringifyAndJoin(",");
    
        static object Part2(State state, ImmutableArray<long> instructions) =>
            RecursiveSolve(instructions, state with { A = 0 }, []).First();
    
        static IEnumerable<long> RecursiveSolve(ImmutableArray<long> instructions, State state, ImmutableList<long> soFar) =>
            (soFar.Count == instructions.Length) ? [state.A] :
            Enumerable.Range(0, 8)
                .Select(a => state with { A = (state.A << 3) + a })
                .Where(newState => newState.A != state.A)
                .Select(newState => new { newState, Execute(instructions, newState).Output, })
                .Where(states => states.Output.SequenceEqual(instructions.TakeLast(states.Output.Count)))
                .SelectMany(states => RecursiveSolve(instructions, states.newState, states.Output));
    
        static State Execute(ImmutableArray<long> instructions, State state)
        {
            while (state.InstPtr < instructions.Length)
            {
                var opcode = instructions[state.InstPtr];
                var operand = instructions[state.InstPtr + 1];
                state = Operations[opcode](state, operand);
            }
    
            return state;
        }
    
        static long ComboOperand(long operand, State state) => operand switch
        {
            >= 0 and <= 3 => operand,
            4 => state.A,
            5 => state.B,
            6 => state.C,
            _ => throw new Exception("Invalid operand."),
        };
    
        static long Adv(long op, State state) => state.A / (long)Math.Pow(2, ComboOperand(op, state));
    
        static readonly Func<State, long, State>[] Operations =
        [
            (s, op) => s with { InstPtr = s.InstPtr + 2, A = Adv(op, s) },
            (s, op) => s with { InstPtr = s.InstPtr + 2, B = s.B ^ op },
            (s, op) => s with { InstPtr = s.InstPtr + 2, B = ComboOperand(op, s) % 8 },
            (s, op) => s with { InstPtr = (s.A == 0) ? (s.InstPtr + 2) : (op <= int.MaxValue) ? (int)op : throw new ArithmeticException("Integer overflow!") },
            (s, _) => s with { InstPtr = s.InstPtr + 2, B = s.B ^ s.C },
            (s, op) => s with { InstPtr = s.InstPtr + 2, Output = s.Output.Add(ComboOperand(op, s) % 8) },
            (s, op) => s with { InstPtr = s.InstPtr + 2, B = Adv(op, s) },
            (s, op) => s with { InstPtr = s.InstPtr + 2, C = Adv(op, s) },
        ];
    
    
        static (State, ImmutableArray<long> instructions) ReceiveInput(string file)
        {
            var input = File.ReadAllLines(file);
    
            return
            (
                new State(
                    long.Parse(input[0].Substring("Register A: ".Length)),
                    long.Parse(input[1].Substring("Register B: ".Length)),
                    long.Parse(input[2].Substring("Register C: ".Length)),
                    0,
                    []),
                input[4].Substring("Program: ".Length)
                    .Split(",")
                    .Select(long.Parse)
                    .ToImmutableArray()
            );
        }
    }
    

  • I’m confused reading the buildQuine() method. It reads to me that when you call it from the top level with top = true, A0 will be set to 0, and then when we get to the 0 to 8 loop, the ‘A’ register will be 0 * 8 + 0 for the first iteration, and then recurse with top = false, but with a0 still ending up 0, causing infinite recursion.

    Am I missing something?

    I got it to work with a check that avoids the recursion if the last state’s A register value is the same as the new state’s value.




  • C#

    Ended up modifying part 1 to do part 2 and return both answers at once.

    using System.Collections.Immutable;
    using System.Diagnostics;
    using Common;
    
    namespace Day16;
    
    static class Program
    {
        static void Main()
        {
            var start = Stopwatch.GetTimestamp();
    
            var smallInput = Input.Parse("smallsample.txt");
            var sampleInput = Input.Parse("sample.txt");
            var programInput = Input.Parse("input.txt");
    
            Console.WriteLine($"Part 1 small: {Solve(smallInput)}");
            Console.WriteLine($"Part 1 sample: {Solve(sampleInput)}");
            Console.WriteLine($"Part 1 input: {Solve(programInput)}");
    
            Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
        }
    
        static (int part1, int part2) Solve(Input i)
        {
            State? endState = null;
            Dictionary<(Point, int), int> lowestScores = new();
    
            var queue = new Queue<State>();
            queue.Enqueue(new State(i.Start, 1, 0, ImmutableHashSet<Point>.Empty));
            while (queue.TryDequeue(out var state))
            {
                if (ElementAt(i.Map, state.Location) is '#')
                {
                    continue;
                }
    
                if (lowestScores.TryGetValue((state.Location, state.DirectionIndex), out var lowestScoreSoFar))
                {
                    if (state.Score > lowestScoreSoFar) continue;
                }
    
                lowestScores[(state.Location, state.DirectionIndex)] = state.Score;
    
                var nextStatePoints = state.Points.Add(state.Location);
    
                if (state.Location == i.End)
                {
                    if ((endState is null) || (state.Score < endState.Score))
                        endState = state with { Points = nextStatePoints };
                    else if (state.Score == endState.Score)
                        endState = state with { Points = nextStatePoints.Union(endState.Points) };
                    continue;
                }
    
                // Walk forward
                queue.Enqueue(state with
                {
                    Location = state.Location.Move(CardinalDirections[state.DirectionIndex]),
                    Score = state.Score + 1,
                    Points = nextStatePoints,
                });
    
                // Turn clockwise
                queue.Enqueue(state with
                {
                    DirectionIndex = (state.DirectionIndex + 1) % CardinalDirections.Length,
                    Score = state.Score + 1000,
                    Points = nextStatePoints,
                });
    
                // Turn counter clockwise
                queue.Enqueue(state with
                {
                    DirectionIndex = (state.DirectionIndex + CardinalDirections.Length - 1) % CardinalDirections.Length,
                    Score = state.Score + 1000,
                    Points = nextStatePoints, 
                });
            }
    
            if (endState is null) throw new Exception("No end state found!");
            return (endState.Score, endState.Points.Count);
        }
    
        public static void DumpMap(Input i, ISet<Point>? points, Point current)
        {
            for (int row = 0; row < i.Bounds.Row; row++)
            {
                for (int col = 0; col < i.Bounds.Col; col++)
                {
                    var p = new Point(row, col);
                    Console.Write(
                        (p == current) ? 'X' :
                        (points?.Contains(p) ?? false) ? 'O' :
                        ElementAt(i.Map, p));
                }
    
                Console.WriteLine();
            }
    
            Console.WriteLine();
        }
    
        public static char ElementAt(string[] map, Point location) => map[location.Row][location.Col];
    
        public record State(Point Location, int DirectionIndex, int Score, ImmutableHashSet<Point> Points);
    
        public static readonly Direction[] CardinalDirections =
            [Direction.Up, Direction.Right, Direction.Down, Direction.Left];
    }
    
    public class Input
    {
        public string[] Map { get; init; } = [];
        public Point Start { get; init; } = new(-1, -1);
        public Point End { get; init; } = new(-1, -1);
        public Point Bounds => new(this.Map.Length, this.Map[0].Length);
    
        public static Input Parse(string file)
        {
            var map = File.ReadAllLines(file);
            Point start = new(-1, -1), end = new(-1, -1);
            foreach (var p in map
                .SelectMany((line, i) => new []
                {
                     new Point(i, line.IndexOf('S')),
                     new Point(i, line.IndexOf('E')),
                })
                .Where(p => p.Col >= 0)
                .Take(2))
            {
                if (map[p.Row][p.Col] is 'S') start = p;
                else end = p;
            }
    
            return new Input()
            {
                Map = map,
                Start = start,
                End = end,
            };
        }
    }
    


  • C#

    Thank goodness for high school algebra!

    using System.Diagnostics;
    using Common;
    
    namespace Day13;
    
    static class Program
    {
        static void Main()
        {
            var start = Stopwatch.GetTimestamp();
    
            var sampleInput = Input.ParseInput("sample.txt");
            var programInput = Input.ParseInput("input.txt");
    
            Console.WriteLine($"Part 1 sample: {Part1(sampleInput)}");
            Console.WriteLine($"Part 1 input: {Part1(programInput)}");
    
            Console.WriteLine($"Part 2 sample: {Part2(sampleInput)}");
            Console.WriteLine($"Part 2 input: {Part2(programInput)}");
    
            Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
        }
    
        static object Part1(Input i) => i.Machines
            .Select(m => CalculateCoins(m, 100))
            .Where(c => c > 0)
            .Sum();
    
        static object Part2(Input i) => i.Machines
            .Select(m => m with { Prize = new XYValues(
                m.Prize.X + 10000000000000,
                m.Prize.Y + 10000000000000) })
            .Select(m => CalculateCoins(m, long.MaxValue))
            .Where(c => c > 0)
            .Sum();
    
        static long CalculateCoins(Machine m, long limit)
        {
            var bBottom = (m.A.X * m.B.Y) - (m.A.Y * m.B.X);
            if (bBottom == 0) return -1;
    
            var bTop = (m.Prize.Y * m.A.X) - (m.Prize.X * m.A.Y);
            var b = bTop / bBottom;
            if ((b <= 0) || (b > limit)) return -1;
            
            var a = (m.Prize.X - (b * m.B.X)) / m.A.X;
            if ((a <= 0) || (a > limit)) return -1;
    
            var calcPrizeX = (a * m.A.X) + (b * m.B.X);
            var calcPrizeY = (a * m.A.Y) + (b * m.B.Y);
            if ((m.Prize.X != calcPrizeX) || (m.Prize.Y != calcPrizeY)) return -1;
    
            return (3 * a) + b;
        }
    }
    
    public record struct Machine(XYValues A, XYValues B, XYValues Prize);
    public record struct XYValues(long X, long Y);
    
    public class Input
    {
        private Input()
        {
        }
    
        public List<Machine> Machines { get; init; }
    
        public static Input ParseInput(string file)
        {
            var machines = new List<Machine>();
    
            var lines = File.ReadAllLines(file);
            for(int l=0; l<lines.Length; l+=4)
            {
                machines.Add(new Machine(
                    ParseXYValues(lines[l + 0]),
                    ParseXYValues(lines[l + 1]),
                    ParseXYValues(lines[l + 2])));
            }
    
            return new Input()
            {
                Machines = machines,
            };
        }
    
        private static readonly char[] XYDelimiters = ['X', 'Y', '=', '+', ',', ' '];
    
        private static XYValues ParseXYValues(string s)
        {
            var parts = s
                .Substring(s.IndexOf(':') + 1)
                .SplitAndTrim(XYDelimiters)
                .Select(long.Parse)
                .ToArray();
            
            return new XYValues(parts[0], parts[1]);
        }
    }
    

  • C#

    There is probably a more efficient way of finding the sides, but this way was what came to me.

    using System.Diagnostics;
    using Common;
    
    namespace Day12;
    
    static class Program
    {
        static void Main()
        {
            var start = Stopwatch.GetTimestamp();
    
            var sampleInput = Input.ParseInput("sample.txt");
            var programInput = Input.ParseInput("input.txt");
    
            var (samplePart1, samplePart2) = Solve(sampleInput);
            Console.WriteLine($"Part 1 sample: {samplePart1}");
            Console.WriteLine($"Part 1 input: {samplePart2}");
    
            var (inputPart1, inputPart2) = Solve(programInput);
            Console.WriteLine($"Part 2 sample: {inputPart1}");
            Console.WriteLine($"Part 2 input: {inputPart2}");
    
            Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
        }
    
        static (int part1, int part2) Solve(Input i)
        {
            var retail = 0;
            var bulk = 0;
            var allPlotPoints = new Dictionary<char, HashSet<Point>>();
            foreach (var p in Grid.EnumerateAllPoints(i.Bounds))
            {
                var plant = i.ElementAt(p);
    
                if (!allPlotPoints.TryGetValue(plant, out var previousPlotPoints))
                {
                    previousPlotPoints = new();
                    allPlotPoints[plant] = previousPlotPoints;
                }
                else if (previousPlotPoints.Contains(p)) continue;
    
                var plotPoints = new HashSet<Point>();
                var perimeter = SearchPlot(i, plotPoints, plant, p);
                var area = plotPoints.Count;
                var sides = CountSides(plotPoints);
                retail += area * perimeter;
                bulk += area * sides;
    
                previousPlotPoints.AddRange(plotPoints);
            }
    
            return (retail, bulk);
        }
    
        static int CountSides(HashSet<Point> plot)
        {
            var sides = 0;
    
            // Track the points we've visited searching for sides
            HashSet<Point> visitedDownRight = new(),
                visitedDownLeft = new(),
                visitedRightDown = new(),
                visitedRightUp = new();
    
            // Sort the points in the plot from upper-left to lower-right, so we can
            // go through them in reading order
            foreach (var p in plot.OrderBy(p => (p.Row * 10000) + p.Col))
            {
                // Move right while looking up
                sides += GetSideLength(p, plot, visitedRightUp, Direction.Right, Direction.Up) > 0 ? 1 : 0;
                
                // Move right while looking down
                sides += GetSideLength(p, plot, visitedRightDown, Direction.Right, Direction.Down) > 0 ? 1 : 0;
                
                // Move down while looking right
                sides += GetSideLength(p, plot, visitedDownRight, Direction.Down, Direction.Right) > 0 ? 1 : 0;
                
                // Move down while looking left
                sides += GetSideLength(p, plot, visitedDownLeft, Direction.Down, Direction.Left) > 0 ? 1 : 0;
            }
    
            return sides;
        }
    
        static int GetSideLength(Point p, HashSet<Point> plotPoints, HashSet<Point> visited, Direction move, Direction look)
        {
            if (!plotPoints.Contains(p)) return 0;
            if (!visited.Add(p)) return 0;
            if (plotPoints.Contains(p.Move(look))) return 0;
            return 1 + GetSideLength(p.Move(move), plotPoints, visited, move, look);
        }
    
        static int SearchPlot(Input i, HashSet<Point> plotPoints, char plant, Point p)
        {
            if (!plotPoints.Add(p)) return 0;
            return p
                .GetCardinalMoves()
                .Select(move =>
                {
                    if (!i.IsInBounds(move) || (i.ElementAt(move) != plant)) return 1;
                    return SearchPlot(i, plotPoints, plant, move);
                })
                .Sum();
        }
    }
    
    public class Input
    {
        public required string[] Map { get; init; }
        
        public Point Bounds => new Point(this.Map.Length, this.Map[0].Length);
        public char ElementAt(Point p) => this.Map[p.Row][p.Col];
        public bool IsInBounds(Point p) => p.IsInBounds(this.Map.Length, this.Map[0].Length);
        
        public static Input ParseInput(string file) => new Input()
        {
            Map = File.ReadAllLines(file),
        };
    }
    

  • I had a very similar take on this problem, but I was not caching the results of a blink for a single stone, like youre doing with subtree_pointers. I tried adding that to my solution, but it didn’t make an appreciable difference. I think that caching the lengths is really the only thing that matters.

    C#

        static object Solve(Input i, int numBlinks)
        {
            // This is a cache of the tuples of (stoneValue, blinks) to
            // the calculated count of their child stones.
            var lengthCache = new Dictionary<(long, int), long>();
            return i.InitialStones
                .Sum(stone => CalculateUltimateLength(stone, numBlinks, lengthCache));
        }
    
        static long CalculateUltimateLength(
            long stone,
            int numBlinks,
            IDictionary<(long, int), long> lengthCache)
        {
            if (numBlinks == 0) return 1;
            
            if (lengthCache.TryGetValue((stone, numBlinks), out var length)) return length;
    
            length = Blink(stone)
                .Sum(next => CalculateUltimateLength(next, numBlinks - 1, lengthCache));
            lengthCache[(stone, numBlinks)] = length;
            return length;
        }
    
        static long[] Blink(long stone)
        {
            if (stone == 0) return [1];
    
            var stoneText = stone.ToString();
            if (stoneText.Length % 2 == 0)
            {
                var halfLength = stoneText.Length / 2;
                return
                [
                    long.Parse(stoneText.Substring(0, halfLength)),
                    long.Parse(stoneText.Substring(halfLength)),
                ];
            }
    
            return [stone * 2024];
        }
    

  • I totally brute forced this one, and it only took about 2 seconds to run, so I call that a win.

    • Keep track of all points visited on part 1.
    • loop over them, placing an extra rock at that position each time through.
    • run part 1
    • if you ever end up at any position you had already visited while facing the same direction, then you’re in a loop.
    • Otherwise, stop if you go off the map.


  • C#

    using System.Diagnostics;
    using Common;
    
    namespace Day10;
    
    static class Program
    {
        static void Main()
        {
            var start = Stopwatch.GetTimestamp();
    
            var sampleInput = Input.ParseInput("sample.txt");
            var programInput = Input.ParseInput("input.txt");
    
            Console.WriteLine($"Part 1 sample: {Part1(sampleInput)}");
            Console.WriteLine($"Part 1 input: {Part1(programInput)}");
    
            Console.WriteLine($"Part 2 sample: {Part2(sampleInput)}");
            Console.WriteLine($"Part 2 input: {Part2(programInput)}");
    
            Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
        }
    
        static object Part1(Input i) => GetTrailheads(i)
            .Sum(th => CountTheNines(th, i, new HashSet<Point>(), false));
    
        static object Part2(Input i) => GetTrailheads(i)
            .Sum(th => CountTheNines(th, i, new HashSet<Point>(), true));
    
        static int CountTheNines(Point loc, Input i, ISet<Point> visited, bool allPaths)
        {
            if (!visited.Add(loc)) return 0;
            
            var result =
                (ElevationAt(loc, i) == 9) ? 1 :
                loc.GetCardinalMoves()
                    .Where(move => move.IsInBounds(i.Bounds.Row, i.Bounds.Col))
                    .Where(move => (ElevationAt(move, i) - ElevationAt(loc, i)) == 1)
                    .Where(move => !visited.Contains(move))
                    .Sum(move => CountTheNines(move, i, visited, allPaths));
            
            if(allPaths) visited.Remove(loc);
            
            return result;
        }
    
        static IEnumerable<Point> GetTrailheads(Input i) => Grid.EnumerateAllPoints(i.Bounds)
            .Where(loc => ElevationAt(loc, i) == 0);
    
        static int ElevationAt(Point p, Input i) => i.Map[p.Row][p.Col];
    }
    
    public class Input
    {
        public required Point Bounds { get; init; }
        public required int[][] Map { get; init; }
        
        public static Input ParseInput(string file)
        {
            using var reader = new StreamReader(file);
            var map = reader.EnumerateLines()
                .Select(l => l.Select(c => (int)(c - '0')).ToArray())
                .ToArray();
            var bounds = new Point(map.Length, map.Max(l => l.Length));
            return new Input()
            {
                Map = map,
                Bounds = bounds,
            };
        }
    }
    


  • C#

    using System.Collections;
    using System.Diagnostics;
    using Common;
    
    namespace Day09;
    
    static class Program
    {
        static void Main()
        {
            var start = Stopwatch.GetTimestamp();
    
            var sampleInput = Input.ParseInput("sample.txt");
            var programInput = Input.ParseInput("input.txt");
    
            Console.WriteLine($"Part 1 sample: {Part1(sampleInput)}");
            Console.WriteLine($"Part 1 input: {Part1(programInput)}");
    
            Console.WriteLine($"Part 2 sample: {Part2(sampleInput)}");
            Console.WriteLine($"Part 2 input: {Part2(programInput)}");
    
            Console.WriteLine($"That took about {Stopwatch.GetElapsedTime(start)}");
        }
    
        static object Part1(Input i)
        {
            var disk = i.Disk.ToList();
            
            while (true)
            {
                // Find the next free space with some blocks open.
                var nextFree = disk.FindIndex(d => (d is Free { Blocks: > 0 }));
                var nextUsed = disk.FindLastIndex(d => (d is Used { Blocks: > 0 }));
    
                if (nextFree > nextUsed) break;
    
                var free = disk[nextFree] as Free ?? throw new Exception("This is not a Free");
                var used = disk[nextUsed] as Used ?? throw new Exception("This is not a Used");
                var canMove = Math.Min(free.Blocks, used.Blocks);
                disk[nextFree] = free with { Blocks = free.Blocks - canMove };
                disk[nextUsed] = used with { Blocks = used.Blocks - canMove };
    
                var addingFree = disk[nextUsed - 1] as Free;
                disk[nextUsed - 1] = addingFree! with { Blocks = addingFree.Blocks + canMove };
                var addingUsed = used! with { Blocks = canMove };
                disk.Insert(nextFree, addingUsed);
            }
    
            // DumpString(disk);
            return CheckSum(disk);
        }
    
        static object Part2(Input i)
        {
            var disk = i.Disk.ToList();
    
            var lastUsedId = int.MaxValue;
            while (true)
            {
                // Find the next free space with some blocks open.
                var nextUsed = disk.FindLastIndex(d => (d is Used { Blocks: > 0 } u) && (u.Id < lastUsedId));
                if (nextUsed < 0) break;
                
                var nextFree = disk.FindIndex(d => (d is Free f) && (f.Blocks >= disk[nextUsed].Blocks));
                var used = disk[nextUsed] as Used ?? throw new Exception("This is not a Used");
                lastUsedId = used.Id;
                if ((nextFree < 0) || (nextFree > nextUsed)) continue; 
    
                var free = disk[nextFree] as Free ?? throw new Exception("This is not a Free");
                var canMove = Math.Min(free.Blocks, used.Blocks);
                disk[nextFree] = free with { Blocks = free.Blocks - canMove };
                disk[nextUsed] = used with { Blocks = used.Blocks - canMove };
    
                var addingFree = disk[nextUsed - 1] as Free;
                disk[nextUsed - 1] = addingFree! with { Blocks = addingFree.Blocks + canMove };
                var addingUsed = used! with { Blocks = canMove };
                disk.Insert(nextFree, addingUsed);
                
                // DumpString(disk);
            }
    
            return CheckSum(disk);
        }
    
        static long CheckSum(IEnumerable<DiskSpace> disk) => disk
            .SelectMany(d => Expand(d))
            .Select((d, i) => (d is Used u) ? (long)(i * u.Id) : 0)
            .Sum();
        
        static IEnumerable<DiskSpace> Expand(DiskSpace d)
        {
            for (int i = 0; i < d.Blocks; i++)
            {
                yield return d with { Blocks = 1 };
            }
        }
    
        static void DumpString(IEnumerable<DiskSpace> disk)
        {
            foreach(var s in disk.Select(d =>
                (d is Used u) ? new string((char)(u.Id + '0'), u.Blocks) :
                (d is Free { Blocks: > 0 } f) ? new string('.', f.Blocks) :
                ""))
            {
                Console.Write(s);
            }
            
            Console.WriteLine();
        }
    }
    
    public abstract record DiskSpace(int Blocks);
    public record Free(int Blocks) : DiskSpace(Blocks);
    public record Used(int Id, int Blocks) : DiskSpace(Blocks);
    
    public class Input
    {
        public DiskSpace[] Disk { get; private init; } = [];
        
        public static Input ParseInput(string file) => new Input()
        {
            Disk = File.ReadAllText(file)
                .TakeWhile(char.IsDigit)
                .Select(c => (int)(c - '0'))
                .Select((c, i) => ((i % 2) == 0) ? (DiskSpace)new Used(i / 2, c) : new Free(c))
                .ToArray(),
        };
    }
    

  • C#

    namespace Day04;
    
    static class Program
    {
        public record struct Point(int Row, int Col);
    
        static void Main(string[] args)
        {
            var sample = File.ReadAllLines("sample.txt");
            var data = File.ReadAllLines("data.txt");
    
            Console.WriteLine($"Part 1 (sample): {SolvePart1(sample)}");
            Console.WriteLine($"Part 1 (data): {SolvePart1(data)}");
    
            Console.WriteLine($"Part 2 (sample): {SolvePart2(sample)}");
            Console.WriteLine($"Part 2 (data): {SolvePart2(data)}");
        }
    
        private static readonly string Search = "XMAS";
    
        private static readonly Func<Point, Point>[] DirectionalMoves =
        {
            p => new Point(p.Row + 1, p.Col),
            p => new Point(p.Row + 1, p.Col + 1),
            p => new Point(p.Row, p.Col + 1),
            p => new Point(p.Row - 1, p.Col + 1),
            p => new Point(p.Row - 1, p.Col),
            p => new Point(p.Row - 1, p.Col - 1),
            p => new Point(p.Row, p.Col - 1),
            p => new Point(p.Row + 1, p.Col - 1),
        };
    
        private static readonly Func<Point, Point>[] ForwardSlashMoves =
        {
            p => new Point(p.Row - 1, p.Col - 1),
            p => new Point(p.Row + 1, p.Col + 1),
        };
        
        private static readonly Func<Point, Point>[] BackSlashMoves =
        {
            p => new Point(p.Row + 1, p.Col - 1),
            p => new Point(p.Row - 1, p.Col + 1),
        };
    
        static long SolvePart1(string[] data)
        {
            return Enumerable
                .Range(0, data.Length)
                .SelectMany(row => Enumerable.Range(0, data[row].Length)
                    .Select(col => new Point(row, col)))
                .Where(p => IsMatch(data, p, Search[0]))
                .Sum(p => DirectionalMoves
                    .Count(move => DeepMatch(data, move(p), move, Search, 1)));
        }
    
        static long SolvePart2(string[] data)
        {
            return Enumerable
                .Range(0, data.Length)
                .SelectMany(row => Enumerable.Range(0, data[row].Length)
                    .Select(col => new Point(row, col)))
                .Where(p => IsMatch(data, p, 'A'))
                .Count(p => CheckDiagonalMoves(data, p, ForwardSlashMoves)
                            && CheckDiagonalMoves(data, p, BackSlashMoves));
        }
    
        static bool CheckDiagonalMoves(string[] data, Point p, Func<Point, Point>[] moves)
            => (IsMatch(data, moves[0](p), 'S') && IsMatch(data, moves[1](p), 'M'))
               || (IsMatch(data, moves[0](p), 'M') && IsMatch(data, moves[1](p), 'S'));
    
        static bool DeepMatch(string[] data, Point p, Func<Point, Point> move, string search, int searchIndex) =>
            (searchIndex >= search.Length) ? true :
            (!IsMatch(data, p, search[searchIndex])) ? false :
            DeepMatch(data, move(p), move, search, searchIndex + 1);
    
        static bool IsMatch(string[] data, Point p, char searchChar)
            => IsInBounds(data, p) && (data[p.Row][p.Col] == searchChar);
    
        static bool IsInBounds(string[] data, Point p) =>
            (p.Row >= 0) && (p.Col >= 0) && (p.Row < data.Length) && (p.Col < data[0].Length);
    }
    

  • C#

    using System;
    using System.Linq;
    
    public record Point(int X, int Y); 
    
    static class Program
    {
        static async Task Main(string[] args)
        {
            var data = (await ReadInputFromFile("data.txt")).ToArray();
    
            var part1Answer = CalculateTotalDifference(data);
            Console.WriteLine($"Part 1 = {part1Answer}");
    
            var part2Answer = CountFrequencies(data);
            Console.WriteLine($"Part 2 = {part2Answer}");
        }
    
        public static int CountFrequencies(ICollection<Point> points)
        {
            var freq = points
                .GroupBy(p => p.Y)
                .ToDictionary(g => g.Key, g => g.Count());
            return points
                .Sum(p => freq.GetValueOrDefault(p.X, 0) * p.X);
        }
    
        public static int CalculateTotalDifference(ICollection<Point> points)
            => points.OrderBy(p => p.X)
                .Zip(
                    points.OrderBy(p => p.Y),
                    (px, py) => Math.Abs(px.X - py.Y))
                .Sum();
    
        public static readonly char[] Delimiter = new char[] { ' ' };
        public static async Task<IEnumerable<Point>> ReadInputFromFile(string path)
            => (await File.ReadAllLinesAsync(path))
                .Select(l =>
                {
                    var parts = l.Split(
                        Delimiter,
                        StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
                    return new Point(int.Parse(parts[0]), int.Parse(parts[1]));
                });
    }