Skip to content

Commit

Permalink
day(21): Keypad Conundrum 🤖
Browse files Browse the repository at this point in the history
  • Loading branch information
adamrodger committed Dec 21, 2024
1 parent 614cf68 commit fb907dd
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 0 deletions.
272 changes: 272 additions & 0 deletions src/AdventOfCode/Day21.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace AdventOfCode
{
/// <summary>
/// Solver for Day 21
/// </summary>
public class Day21
{
public int Part1(string[] input)
{
// 32811 low
// 163246 high
// 159558 high
return input.Sum(i => int.Parse(i[..^1]) * (int)FewestPresses(i, 3));
}

public long Part2(string[] input)
{
// 361374212849220 high
// 196166290013580 high
return input.Sum(i => int.Parse(i[..^1]) * FewestPresses(i, 26));
}

/// <summary>
/// Count the fewest number of key presses required to trigger the given sequence of presses
/// </summary>
/// <param name="sequence">Sequence of key presses to check</param>
/// <param name="totalRobots">Total number of robots between you and the numeric pad</param>
/// <returns>Number of key presses required</returns>
private static long FewestPresses(string sequence, int totalRobots)
=> Presses(sequence, 0, totalRobots, new Dictionary<(string, int), long>());

/// <summary>
/// Count the fewest number of key presses required to trigger the given sequence of presses
/// </summary>
/// <param name="sequence">Sequence of key presses to check</param>
/// <param name="depth">Current depth, where depth 0 is the numeric pad and every depth after that is a directional pad</param>
/// <param name="maxDepth">Maximum depth, which is number of intermediate robots, plus one for the human and one for the numeric pad</param>
/// <param name="cache">Cache</param>
/// <returns>Number of key presses required</returns>
private static long Presses(string sequence, int depth, int maxDepth, Dictionary<(string, int), long> cache)
{
if (depth == maxDepth)
{
return sequence.Length;
}

if (cache.TryGetValue((sequence, depth), out long value))
{
return value;
}

value = ('A' + sequence).Zip(sequence)
.Select(pair => depth == 0
? NumpadSequences(pair.First, pair.Second)
: DirectionPadSequences(pair.First, pair.Second))
.Sum(possible => possible.Min(next => Presses(next + 'A', depth + 1, maxDepth, cache)));
cache[(sequence, depth)] = value;
return value;
}

/// <summary>
/// +---+---+---+
/// | 7 | 8 | 9 |
/// +---+---+---+
/// | 4 | 5 | 6 |
/// +---+---+---+
/// | 1 | 2 | 3 |
/// +---+---+---+
/// | 0 | A |
/// +---+---+
/// </summary>
/// <remarks>
/// Observed that to be most efficient, you must:
///
/// - move left before up
/// - move up before right
/// - move right before down (except 2 -> A which is the opposite for some reason)
/// - move down before left
///
/// You must always take the maximum number of steps in a direction, never zigzagging
///
/// Sometimes it's not possible to follow those rules, because you'd have to pass through the disallowed
/// bottom left square, so then we flip the order of horizontal and vertical moves, e.g. A -> 1
///
/// I'm leaving all the possibilities in because it's too hard to prove/debug the above. It could be the case
/// that my input and the sample inputs just don't exercise some of these, and those may be exceptions like
/// the 2 -> A exception that my real input definitely needs.
/// </remarks>
private static string[] NumpadSequences(char start, char end) => (start, end) switch
{
('A', 'A') => [""],
('A', '0') => ["<"],
('A', '1') => ["^<<"],
('A', '2') => ["<^'", "^<"],
('A', '3') => ["^"],
('A', '4') => ["^^<<"],
('A', '5') => ["<^^", "^^<"],
('A', '6') => ["^^"],
('A', '7') => ["^^^<<"],
('A', '8') => ["<^^^", "^^^<"],
('A', '9') => ["^^^"],

('0', 'A') => [">"],
('0', '0') => [""],
('0', '1') => ["^<"],
('0', '2') => ["^"],
('0', '3') => [">^", "^>"],
('0', '4') => ["^^<"],
('0', '5') => ["^^"],
('0', '6') => [">^^", "^^>"],
('0', '7') => ["^^^<"],
('0', '8') => ["^^^"],
('0', '9') => [">^^^", "^^^>"],

('1', 'A') => [">>v"],
('1', '0') => [">v"],
('1', '1') => [""],
('1', '2') => [">"],
('1', '3') => [">>"],
('1', '4') => ["^"],
('1', '5') => [">^", "^>"],
('1', '6') => [">>^", "^>>"],
('1', '7') => ["^^"],
('1', '8') => [">^^", "^^>"],
('1', '9') => [">>^^", "^^>>"],

('2', 'A') => [">v", "v>"],
('2', '0') => ["v"],
('2', '1') => ["<"],
('2', '2') => [""],
('2', '3') => [">"],
('2', '4') => ["<^", "^<"],
('2', '5') => ["^"],
('2', '6') => [">^", "^>"],
('2', '7') => ["<^^", "^^<"],
('2', '8') => ["^^"],
('2', '9') => [">^^", "^^>"],

('3', 'A') => ["v"],
('3', '0') => ["<v", "v<"],
('3', '1') => ["<<"],
('3', '2') => ["<"],
('3', '3') => [""],
('3', '4') => ["<<^", "^<<"],
('3', '5') => ["<^", "^<"],
('3', '6') => ["^"],
('3', '7') => ["<<^^", "^^<<"],
('3', '8') => ["<^^", "^^<"],
('3', '9') => ["^^"],

('4', 'A') => [">>vv"],
('4', '0') => [">vv"],
('4', '1') => ["v"],
('4', '2') => [">v", "v>"],
('4', '3') => [">>v", "v>>"],
('4', '4') => [""],
('4', '5') => [">"],
('4', '6') => [">>"],
('4', '7') => ["^"],
('4', '8') => [">^", "^>"],
('4', '9') => [">>^", "^>>"],

('5', 'A') => [">vv", "vv>"],
('5', '0') => ["vv"],
('5', '1') => ["<v", "v<"],
('5', '2') => ["v"],
('5', '3') => [">v", "v>"],
('5', '4') => ["<"],
('5', '5') => [""],
('5', '6') => [">"],
('5', '7') => ["<^", "^<"],
('5', '8') => ["^"],
('5', '9') => [">^", "^>"],

('6', 'A') => ["vv"],
('6', '0') => ["<vv", "vv<"],
('6', '1') => ["<<v", "v<<"],
('6', '2') => ["<v", "v<"],
('6', '3') => ["v"],
('6', '4') => ["<<"],
('6', '5') => ["<"],
('6', '6') => [""],
('6', '7') => ["<<^", "^<<"],
('6', '8') => ["<^", "^<"],
('6', '9') => ["^"],

('7', 'A') => [">>vvv"],
('7', '0') => [">vvv"],
('7', '1') => ["vv"],
('7', '2') => [">vv", "vv>"],
('7', '3') => [">>vv", "vv>>"],
('7', '4') => ["v"],
('7', '5') => [">v", "v>"],
('7', '6') => [">>v", "v>>"],
('7', '7') => [""],
('7', '8') => [">"],
('7', '9') => [">>"],

('8', 'A') => [">vvv", "vvv>"],
('8', '0') => ["vvv"],
('8', '1') => ["<vv", "vv<"],
('8', '2') => ["vv"],
('8', '3') => [">vv", "vv>"],
('8', '4') => ["<v", "v<"],
('8', '5') => ["v"],
('8', '6') => [">v", "v>"],
('8', '7') => ["<"],
('8', '8') => [""],
('8', '9') => [">"],

('9', 'A') => ["vvv"],
('9', '0') => ["<vvv", "vvv<"],
('9', '1') => ["<<vv", "vv<<"],
('9', '2') => ["<vv", "vv<"],
('9', '3') => ["vv"],
('9', '4') => ["<<v", "v<<"],
('9', '5') => ["<v", "v<"],
('9', '6') => ["v"],
('9', '7') => ["<<"],
('9', '8') => ["<"],
('9', '9') => [""],

_ => throw new ArgumentOutOfRangeException()
};

/// <summary>
/// +---+---+
/// | ^ | A |
/// +---+---+---+
/// | < | v | > |
/// +---+---+---+
/// </summary>
private static string[] DirectionPadSequences(char start, char end) => (start, end) switch
{
('A', '<') => ["v<<"],
('A', 'v') => ["<v", "v<"],
('A', '>') => ["v"],
('A', '^') => ["<"],
('A', 'A') => [""],

('^', '<') => ["v<"],
('^', 'v') => ["v"],
('^', '>') => [">v", "v>"],
('^', '^') => [""],
('^', 'A') => [">"],

('v', '<') => ["<"],
('v', 'v') => [""],
('v', '>') => [">"],
('v', '^') => ["^"],
('v', 'A') => [">^", "^>"],

('<', '<') => [""],
('<', 'v') => [">"],
('<', '>') => [">>"],
('<', '^') => [">^"],
('<', 'A') => [">>^"],

('>', '<') => ["<<"],
('>', 'v') => ["<"],
('>', '>') => [""],
('>', '^') => ["<^", "^<"],
('>', 'A') => ["^"],

_ => throw new ArgumentOutOfRangeException()
};
}
}
Binary file added src/AdventOfCode/inputs/day21.txt
Binary file not shown.
68 changes: 68 additions & 0 deletions tests/AdventOfCode.Tests/Day21Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.IO;
using Xunit;
using Xunit.Abstractions;

namespace AdventOfCode.Tests
{
public class Day21Tests
{
private readonly ITestOutputHelper output;
private readonly Day21 solver;

public Day21Tests(ITestOutputHelper output)
{
this.output = output;
this.solver = new Day21();
}

private static string[] GetRealInput()
{
string[] input = File.ReadAllLines("inputs/day21.txt");
return input;
}

private static string[] GetSampleInput()
{
return new string[]
{
"029A",
"980A",
"179A",
"456A",
"379A",
};
}

[Fact]
public void Part1_SampleInput_ProducesCorrectResponse()
{
var expected = 126384;

var result = solver.Part1(GetSampleInput());

Assert.Equal(expected, result);
}

[Fact]
public void Part1_RealInput_ProducesCorrectResponse()
{
var expected = 157230;

var result = solver.Part1(GetRealInput());
output.WriteLine($"Day 21 - Part 1 - {result}");

Assert.Equal(expected, result);
}

[Fact]
public void Part2_RealInput_ProducesCorrectResponse()
{
var expected = 195969155897936;

var result = solver.Part2(GetRealInput());
output.WriteLine($"Day 21 - Part 2 - {result}");

Assert.Equal(expected, result);
}
}
}

0 comments on commit fb907dd

Please sign in to comment.