diff --git a/src/21.cs b/src/21.cs new file mode 100644 index 0000000..47c0fb4 --- /dev/null +++ b/src/21.cs @@ -0,0 +1,272 @@ +using aoc2024.Util; + +namespace aoc2024; + +internal class Day21 : Day +{ + private List codes = []; + + private static readonly Dictionary numericButtonLayout = + new() + { + {'7', new ivec2(0, 0)}, + {'8', new ivec2(1, 0)}, + {'9', new ivec2(2, 0)}, + {'4', new ivec2(0, 1)}, + {'5', new ivec2(1, 1)}, + {'6', new ivec2(2, 1)}, + {'1', new ivec2(0, 2)}, + {'2', new ivec2(1, 2)}, + {'3', new ivec2(2, 2)}, + {'0', new ivec2(1, 3)}, + {'A', new ivec2(2, 3)}, + }; + + private static readonly Dictionary reverseNumericButtonLayout = + new() + { + {new ivec2(0, 0), '7'}, + {new ivec2(1, 0), '8'}, + {new ivec2(2, 0), '9'}, + {new ivec2(0, 1), '4'}, + {new ivec2(1, 1), '5'}, + {new ivec2(2, 1), '6'}, + {new ivec2(0, 2), '1'}, + {new ivec2(1, 2), '2'}, + {new ivec2(2, 2), '3'}, + {new ivec2(1, 3), '0'}, + {new ivec2(2, 3), 'A'}, + }; + + private static readonly Dictionary directionalKeypadLayout = + new() + { + {'^', new ivec2(1, 0)}, + {'A', new ivec2(2, 0)}, + {'<', new ivec2(0, 1)}, + {'v', new ivec2(1, 1)}, + {'>', new ivec2(2, 1)}, + }; + + private static readonly Dictionary reverseDirectionalKeypadLayout = + new() + { + {new ivec2(1, 0), '^'}, + {new ivec2(2, 0), 'A'}, + {new ivec2(0, 1), '<'}, + {new ivec2(1, 1), 'v'}, + {new ivec2(2, 1), '>'}, + }; + + private static readonly Dictionary<(char from, char to), List> numericKeypadMoves = GetMoves(numericButtonLayout); + private static readonly Dictionary<(char from, char to), List> directionalKeypadMoves = GetMoves(directionalKeypadLayout); + private static readonly Dictionary<(char from, char to), List> allMoves = numericKeypadMoves.Where(k => !directionalKeypadMoves.ContainsKey(k.Key)).Union(directionalKeypadMoves).ToDictionary(); + + internal override void Parse() + { + codes = [..Util.Parsing.ReadAllLines($"{GetDay()}")]; + } + + private static ivec2? GetPosFromKey(char key, Dictionary layout) + { + if (!layout.TryGetValue(key, out var pos)) + { + return null; + } + + return pos; + } + + private static char? GetKeyFromPos(ivec2 pos, Dictionary layout) + { + var dict = reverseDirectionalKeypadLayout; + if (layout.Count == numericButtonLayout.Count) + { + dict = reverseNumericButtonLayout; + } + + if (!dict.TryGetValue(pos, out var key)) + { + return null; + } + + return key; + } + + private static Dictionary<(char from, char to), List> GetMoves(Dictionary keypad) + { + var keys = string.Concat(keypad.Select(k => k.Key)); + + Dictionary<(char from, char to), List> keypadMoves = []; + for (int fromIdx = 0; fromIdx < keys.Length; fromIdx++) + { + char from = keys[fromIdx]; + var keyPos = GetPosFromKey(from, keypad); + + for (int toIdx = fromIdx; toIdx < keys.Length; toIdx++) + { + char to = keys[toIdx]; + + var pq = new PriorityQueue(); + pq.Enqueue(keyPos!.Value, 0); + + Dictionary options)> state = []; + state.Add(keyPos.Value, (0, [""])); + + while (pq.TryDequeue(out var pos, out var _)) + { + var (cost, options) = state[pos]; + if (GetKeyFromPos(pos, keypad) == to) + { + keypadMoves.Add((from, to), [.. options]); + if (from != to) + { + HashSet reverseOptions = []; + foreach (string s1 in options) + { + string rev = s1.Reverse() + .Aggregate("", (current, c) => current + c switch + { + '>' => '<', + '<' => ">", + '^' => 'v', + 'v' => '^', + _ => c, + }); + + reverseOptions.Add(rev); + } + + keypadMoves.Add((to, from), [.. reverseOptions]); + } + break; + } + + foreach (var neighbor in pos.GetBoundedOrthogonalNeighbors(0, 0, keypad.Max(k => k.Value.x), keypad.Max(k => k.Value.y))) + { + if (GetKeyFromPos(pos, keypad) == null) + { + continue; + } + + var newCost = cost + 1; + var hasSeen = state.TryGetValue(neighbor, out var thisState); + if (!hasSeen) + { + thisState = (newCost, []); + state[neighbor] = thisState; + } + + if (newCost != thisState.cost) + { + continue; + } + + foreach (string s in options) + { + thisState.options.Add(s + ivec2.Sign(neighbor - pos).CharFromDir()); + } + + if (!hasSeen) + { + pq.Enqueue(neighbor, newCost); + } + } + } + } + } + + return keypadMoves; + } + + private static List GetSequenceList(string code, Dictionary<(char from, char to), List> keypadMoves) + { + List sequence = [""]; + char prevKey = 'A'; + + foreach (var key in code) + { + var newSeq = new List(); + var moves = keypadMoves[(prevKey, key)]; + + foreach (var prevStrokes in sequence) + { + foreach (var nextStroke in moves) + { + newSeq.Add(prevStrokes + nextStroke + 'A'); + } + } + + prevKey = key; + sequence = newSeq; + } + + return sequence; + } + + private static long ShortestMovesTo(string current, int level, int stopLevel, Dictionary<(string key, int level), long> cache) + { + if (cache.TryGetValue((current, level), out var cost)) + { + return cost; + } + + if (level == stopLevel) + { + var result = GetSequenceList(current, allMoves).Select(a => a.Length).Min(); + cache.Add((current, level), result); + return result; + } + + var firstMoveIdx = current.IndexOf('A'); + var firstMove = current[..(firstMoveIdx + 1)]; + var remainingMoves = current[(firstMoveIdx + 1)..]; + + long shortest = long.MaxValue; + var sequences = GetSequenceList(firstMove, allMoves); + foreach (var seq in sequences) + { + long count = ShortestMovesTo(seq, level + 1, stopLevel, cache); + if (shortest > count) + { + shortest = count; + } + } + + if (remainingMoves.Length > 0) + { + shortest += ShortestMovesTo(remainingMoves, level, stopLevel, cache); + } + + cache.Add((current, level), shortest); + return shortest; + } + + internal override string Part1() + { + var cache = new Dictionary<(string key, int level), long>(); + long total = 0; + foreach (var code in codes) + { + var num = int.Parse(code[..3]); + var moveLen = ShortestMovesTo(code, 0, 2, cache); + total += moveLen * num; + } + + return $"3-level code complexity sum: <+white>{total}"; + } + + internal override string Part2() + { + var cache = new Dictionary<(string key, int level), long>(); + long total = 0; + foreach (var code in codes) + { + var num = int.Parse(code[..3]); + var moveLen = ShortestMovesTo(code, 0, 25, cache); + total += moveLen * num; + } + + return $"26-level code complexity sum: <+white>{total}"; + } +} \ No newline at end of file diff --git a/src/Util/Vec2.cs b/src/Util/Vec2.cs index 254a37b..8efdb64 100644 --- a/src/Util/Vec2.cs +++ b/src/Util/Vec2.cs @@ -38,6 +38,8 @@ public readonly struct ivec2 : IEquatable, IComparable, IComparabl public long ManhattanDistance => Abs(this).Sum; public long ManhattanDistanceTo(ivec2 other) => System.Math.Abs(x - other.x) + System.Math.Abs(y - other.y); + public ivec2 ManhattanVectorTo(ivec2 other) => new(other.x - x, other.y - y); + public static ivec2 DirFromChar(char ch) => ch switch { '^' => UP, @@ -47,6 +49,31 @@ public readonly struct ivec2 : IEquatable, IComparable, IComparabl _ => throw new FormatException($"Invalid direction {ch}"), }; + public char CharFromDir() + { + if (this == UP) + { + return '^'; + } + + if (this == RIGHT) + { + return '>'; + } + + if (this == LEFT) + { + return '<'; + } + + if (this == DOWN) + { + return 'v'; + } + + throw new FormatException($"Invalid direction vector {this}"); + } + public ivec2 GetBestDirectionTo(ivec2 p) { ivec2 diff = p - this;