|
1 | 1 | //! # Keypad Conundrum |
| 2 | +//! |
| 3 | +//! Each key sequence always end in `A`. This means that we can consider each group of button |
| 4 | +//! presses between `A`s independently using a recursive approach with memoization to efficiently |
| 5 | +//! compute the minimum presses needed for any depth of chained robots. |
2 | 6 | use crate::util::hash::*; |
3 | 7 | use crate::util::parse::*; |
4 | 8 | use crate::util::point::*; |
| 9 | +use std::iter::{once, repeat_n}; |
5 | 10 |
|
6 | | -type Cache = FastMap<(usize, usize), usize>; |
| 11 | +type Input = (Vec<(String, usize)>, Combinations); |
| 12 | +type Combinations = FastMap<(char, char), Vec<String>>; |
| 13 | +type Cache = FastMap<(char, char, usize), usize>; |
7 | 14 |
|
8 | | -pub fn parse(input: &str) -> &str { |
9 | | - input |
| 15 | +/// Convert codes to pairs of the sequence itself with the numeric part. |
| 16 | +/// The pad combinations are the same between both parts so only need to be computed once. |
| 17 | +pub fn parse(input: &str) -> Input { |
| 18 | + let pairs = input.lines().map(String::from).zip(input.iter_unsigned()).collect(); |
| 19 | + (pairs, pad_combinations()) |
10 | 20 | } |
11 | 21 |
|
12 | | -pub fn part1(input: &str) -> usize { |
| 22 | +pub fn part1(input: &Input) -> usize { |
13 | 23 | chain(input, 3) |
14 | 24 | } |
15 | 25 |
|
16 | | -pub fn part2(input: &str) -> usize { |
| 26 | +pub fn part2(input: &Input) -> usize { |
17 | 27 | chain(input, 26) |
18 | 28 | } |
19 | 29 |
|
20 | | -fn chain(input: &str, limit: usize) -> usize { |
| 30 | +fn chain(input: &Input, depth: usize) -> usize { |
| 31 | + let (pairs, combinations) = input; |
21 | 32 | let cache = &mut FastMap::with_capacity(500); |
22 | | - input |
23 | | - .lines() |
24 | | - .map(str::as_bytes) |
25 | | - .zip(input.iter_unsigned::<usize>()) |
26 | | - .map(|(code, numeric)| dfs(cache, code, 0, limit) * numeric) |
27 | | - .sum() |
| 33 | + pairs.iter().map(|(code, numeric)| dfs(cache, combinations, code, depth) * numeric).sum() |
28 | 34 | } |
29 | 35 |
|
30 | | -fn dfs(cache: &mut Cache, slice: &[u8], depth: usize, limit: usize) -> usize { |
31 | | - if depth == limit { |
32 | | - return slice.len(); |
| 36 | +fn dfs(cache: &mut Cache, combinations: &Combinations, code: &str, depth: usize) -> usize { |
| 37 | + // Number of presses for the last keypad is just the length of the sequence. |
| 38 | + if depth == 0 { |
| 39 | + return code.len(); |
33 | 40 | } |
34 | 41 |
|
35 | | - let key = (to_usize(slice), depth); |
36 | | - if let Some(&previous) = cache.get(&key) { |
37 | | - return previous; |
| 42 | + // All keypads start with `A`, either the initial position of the keypad or the trailing `A` |
| 43 | + // from the previous sequence at this level. |
| 44 | + let mut previous = 'A'; |
| 45 | + let mut result = 0; |
| 46 | + |
| 47 | + for current in code.chars() { |
| 48 | + // Check each pair of characters, memoizing results. |
| 49 | + let key = (previous, current, depth); |
| 50 | + |
| 51 | + result += cache.get(&key).copied().unwrap_or_else(|| { |
| 52 | + // Each transition has either 1 or 2 possibilities. |
| 53 | + // Pick the sequence that results in the minimum keypresses. |
| 54 | + let presses = combinations[&(previous, current)] |
| 55 | + .iter() |
| 56 | + .map(|next| dfs(cache, combinations, next, depth - 1)) |
| 57 | + .min() |
| 58 | + .unwrap(); |
| 59 | + cache.insert(key, presses); |
| 60 | + presses |
| 61 | + }); |
| 62 | + |
| 63 | + previous = current; |
38 | 64 | } |
39 | 65 |
|
40 | | - let keypad = if depth == 0 { NUMERIC } else { DIRECTIONAL }; |
41 | | - let mut shortest = usize::MAX; |
42 | | - |
43 | | - for sequence in combinations(slice, &keypad) { |
44 | | - let mut presses = 0; |
45 | | - |
46 | | - for chunk in sequence.split_inclusive(|&b| b == b'A') { |
47 | | - presses += dfs(cache, chunk, depth + 1, limit); |
48 | | - } |
49 | | - |
50 | | - shortest = shortest.min(presses); |
51 | | - } |
52 | | - |
53 | | - cache.insert(key, shortest); |
54 | | - shortest |
| 66 | + result |
55 | 67 | } |
56 | 68 |
|
57 | | -fn combinations(current: &[u8], keypad: &Keypad) -> Vec<Vec<u8>> { |
58 | | - let mut next = Vec::new(); |
59 | | - pad_dfs(&mut next, &mut Vec::with_capacity(16), keypad, current, 0, keypad.start); |
60 | | - next |
| 69 | +/// Compute keypresses needed for all possible transitions for both numeric and directional |
| 70 | +/// keypads. There are no distinct pairs shared between the keypads so they can use the same map |
| 71 | +/// without conflict. |
| 72 | +fn pad_combinations() -> Combinations { |
| 73 | + let numeric_gap = Point::new(0, 3); |
| 74 | + let numeric_keys = [ |
| 75 | + ('7', Point::new(0, 0)), |
| 76 | + ('8', Point::new(1, 0)), |
| 77 | + ('9', Point::new(2, 0)), |
| 78 | + ('4', Point::new(0, 1)), |
| 79 | + ('5', Point::new(1, 1)), |
| 80 | + ('6', Point::new(2, 1)), |
| 81 | + ('1', Point::new(0, 2)), |
| 82 | + ('2', Point::new(1, 2)), |
| 83 | + ('3', Point::new(2, 2)), |
| 84 | + ('0', Point::new(1, 3)), |
| 85 | + ('A', Point::new(2, 3)), |
| 86 | + ]; |
| 87 | + |
| 88 | + let directional_gap = Point::new(0, 0); |
| 89 | + let directional_keys = [ |
| 90 | + ('^', Point::new(1, 0)), |
| 91 | + ('A', Point::new(2, 0)), |
| 92 | + ('<', Point::new(0, 1)), |
| 93 | + ('v', Point::new(1, 1)), |
| 94 | + ('>', Point::new(2, 1)), |
| 95 | + ]; |
| 96 | + |
| 97 | + let mut combinations = FastMap::with_capacity(145); |
| 98 | + pad_routes(&mut combinations, &numeric_keys, numeric_gap); |
| 99 | + pad_routes(&mut combinations, &directional_keys, directional_gap); |
| 100 | + combinations |
61 | 101 | } |
62 | 102 |
|
63 | | -fn pad_dfs( |
64 | | - combinations: &mut Vec<Vec<u8>>, |
65 | | - path: &mut Vec<u8>, |
66 | | - keypad: &Keypad, |
67 | | - sequence: &[u8], |
68 | | - depth: usize, |
69 | | - from: Point, |
70 | | -) { |
71 | | - // Success |
72 | | - if depth == sequence.len() { |
73 | | - combinations.push(path.clone()); |
74 | | - return; |
75 | | - } |
76 | | - |
77 | | - // Failure |
78 | | - if from == keypad.gap { |
79 | | - return; |
80 | | - } |
81 | | - |
82 | | - let to = keypad.lookup[sequence[depth] as usize]; |
83 | | - |
84 | | - if from == to { |
85 | | - // Push button. |
86 | | - path.push(b'A'); |
87 | | - pad_dfs(combinations, path, keypad, sequence, depth + 1, from); |
88 | | - path.pop(); |
89 | | - } else { |
90 | | - // Move towards button. |
91 | | - let mut step = |next: u8, direction: Point| { |
92 | | - path.push(next); |
93 | | - pad_dfs(combinations, path, keypad, sequence, depth, from + direction); |
94 | | - path.pop(); |
95 | | - }; |
96 | | - |
97 | | - if to.x < from.x { |
98 | | - step(b'<', LEFT); |
99 | | - } |
100 | | - if to.x > from.x { |
101 | | - step(b'>', RIGHT); |
102 | | - } |
103 | | - if to.y < from.y { |
104 | | - step(b'^', UP); |
105 | | - } |
106 | | - if to.y > from.y { |
107 | | - step(b'v', DOWN); |
| 103 | +/// Each route between two keys has 2 possibilites, horizontal first or vertical first. |
| 104 | +/// We skip any route that would cross the gap and also avoid adding the same route twice |
| 105 | +/// when a key is in a straight line (e.g. directly above/below or left/right). For example: |
| 106 | +/// |
| 107 | +/// * `7 => A` is only `>>vvv`. |
| 108 | +/// * `1 => 5` is `^>` and `>^`. |
| 109 | +/// |
| 110 | +/// We don't consider routes that change direction more than once as these are always longer, |
| 111 | +/// for example `5 => A` ignores the path `v>v`. |
| 112 | +fn pad_routes(combinations: &mut Combinations, pad: &[(char, Point)], gap: Point) { |
| 113 | + for &(first, from) in pad { |
| 114 | + for &(second, to) in pad { |
| 115 | + let horizontal = || { |
| 116 | + let element = if from.x < to.x { '>' } else { '<' }; |
| 117 | + let count = from.x.abs_diff(to.x) as usize; |
| 118 | + repeat_n(element, count) |
| 119 | + }; |
| 120 | + |
| 121 | + let vertical = || { |
| 122 | + let element = if from.y < to.y { 'v' } else { '^' }; |
| 123 | + let count = from.y.abs_diff(to.y) as usize; |
| 124 | + repeat_n(element, count) |
| 125 | + }; |
| 126 | + |
| 127 | + if Point::new(from.x, to.y) != gap { |
| 128 | + let path = vertical().chain(horizontal()).chain(once('A')).collect(); |
| 129 | + combinations.entry((first, second)).or_default().push(path); |
| 130 | + } |
| 131 | + |
| 132 | + if from.x != to.x && from.y != to.y && Point::new(to.x, from.y) != gap { |
| 133 | + let path = horizontal().chain(vertical()).chain(once('A')).collect(); |
| 134 | + combinations.entry((first, second)).or_default().push(path); |
| 135 | + } |
108 | 136 | } |
109 | 137 | } |
110 | 138 | } |
111 | | - |
112 | | -struct Keypad { |
113 | | - start: Point, |
114 | | - gap: Point, |
115 | | - lookup: [Point; 128], |
116 | | -} |
117 | | - |
118 | | -const NUMERIC: Keypad = { |
119 | | - let start = Point::new(2, 3); |
120 | | - let gap = Point::new(0, 3); |
121 | | - let mut lookup = [ORIGIN; 128]; |
122 | | - |
123 | | - lookup[b'7' as usize] = Point::new(0, 0); |
124 | | - lookup[b'8' as usize] = Point::new(1, 0); |
125 | | - lookup[b'9' as usize] = Point::new(2, 0); |
126 | | - lookup[b'4' as usize] = Point::new(0, 1); |
127 | | - lookup[b'5' as usize] = Point::new(1, 1); |
128 | | - lookup[b'6' as usize] = Point::new(2, 1); |
129 | | - lookup[b'1' as usize] = Point::new(0, 2); |
130 | | - lookup[b'2' as usize] = Point::new(1, 2); |
131 | | - lookup[b'3' as usize] = Point::new(2, 2); |
132 | | - lookup[b'0' as usize] = Point::new(1, 3); |
133 | | - lookup[b'A' as usize] = Point::new(2, 3); |
134 | | - |
135 | | - Keypad { start, gap, lookup } |
136 | | -}; |
137 | | - |
138 | | -const DIRECTIONAL: Keypad = { |
139 | | - let start = Point::new(2, 0); |
140 | | - let gap = Point::new(0, 0); |
141 | | - let mut lookup = [ORIGIN; 128]; |
142 | | - |
143 | | - lookup[b'^' as usize] = Point::new(1, 0); |
144 | | - lookup[b'A' as usize] = Point::new(2, 0); |
145 | | - lookup[b'<' as usize] = Point::new(0, 1); |
146 | | - lookup[b'v' as usize] = Point::new(1, 1); |
147 | | - lookup[b'>' as usize] = Point::new(2, 1); |
148 | | - |
149 | | - Keypad { start, gap, lookup } |
150 | | -}; |
151 | | - |
152 | | -// Max slice length is 5 so value is unique. |
153 | | -fn to_usize(slice: &[u8]) -> usize { |
154 | | - let mut array = [0; 8]; |
155 | | - array[0..slice.len()].copy_from_slice(slice); |
156 | | - usize::from_ne_bytes(array) |
157 | | -} |
0 commit comments