|
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::new(); |
| 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