After a relatively easy part 1, the part 2 was the most challenging since the beginning of this Advent of Code.
I spent about 50 minutes for part 1, and nearly 3 hours for part 2. My ranks are roughly the same for both parts, just below the 10000th place. As I am writing this article, at 10 pm (UTC +1), more than 30 % of part 1 solvers have not been able to solve the part 2.
Problem description
The problem objective is to decode signals, and to re-order 7-segment displays. Each line in the input file is composed of a series of 10 signal patterns followed by a delimiter and a second series of 4 patterns. In the first series, the patterns represent the 10 different digits, and each pattern indicates which segments of a 7-segment display are on (between 2 and 7 segments). The second series represent a 4-digit output value, using the same code as the input series.
Below is an example of a input file line.
acedgfb cdfbe gcdfa fbcad dab cefabd cdfgeb eafb cagedb ab | cdfeb fcadb cdfeb cdbaf
The correspondance between digits and patterns is the following :
| Pattern | Digit |
| acedgfb | 8 |
| cdfbe | 5 |
| gcdfa | 2 |
| fbcad | 3 |
| dab | 7 |
| cefabd | 9 |
| cdfgeb | 6 |
| eafb | 4 |
| cagedb | 0 |
| ab | 1 |
Therefore, the output number is 5353.
Note that each line of the input data file has a different code.
Data structures
Input and output signals are separated in two distinct vectors.
#[derive(Clone)]
struct Entry {
input_signal: Vec<String>,
output_signal: Vec<String>
}
impl Entry {
pub fn new(input: String) -> Self {
let entry_parts = input.split(" | ").collect::<Vec<&str>>();
Entry { input_signal: entry_parts[0].split(" ")
.map(|x| String::from(x)).collect::<Vec<String>>(),
output_signal: entry_parts[1].split(" ")
.map(|x| String::from(x)).collect::<Vec<String>>() }
}
}
Part 1 (easy)
The goal is to count output digits equal to 1, 4, 7 or 8. The simple idea is that these digits can be recognized by their number of segments (for example, the digit 1 is the only one with 2 segments)
We use the following table :
| Digit | Number of segments |
| 0 | 6 |
| 1 | 2 |
| 2 | 5 |
| 3 | 5 |
| 4 | 4 |
| 5 | 5 |
| 6 | 6 |
| 7 | 3 |
| 8 | 7 |
| 9 | 6 |
and we add the following method to Entry
pub fn count_easy_digits(&self) -> usize {
self.output_signal.iter().cloned()
.filter(|x| x.len() == 2 || x.len() == 3 ||
x.len() == 4 || x.len() == 7).count()
}
The main function sums the outcomes of count_easy_digits() for each line.
pub fn solve_part1(input: &Vec<String>) -> usize {
let mut easy_digits_count = 0;
for line in input {
easy_digits_count += Entry::new(line.to_string()).count_easy_digits();
}
easy_digits_count
}
Part 2 (challenging)
We write a method correspondence_table(), that returns a vector of 10 elements. Each element is a
String, containing the digit segments.
The decoding operation is done in 3 steps !
- Step 1 : identify the “easy digits”. This is by far the easiest task
let mut correspondence = vec![""; 10];
for pattern in &self.input_signal {
match pattern.len() {
2 => { correspondence[1] = pattern; }
3 => { correspondence[7] = pattern; }
4 => { correspondence[4] = pattern; }
7 => { correspondence[8] = pattern; }
_ => {}
}
}
- Step 2 : identify the digit 1 segments. This will allow us to identify all the 5-segments digits (i.e 2, 3 and 5) in the next step.
let segment1_count = self.input_signal.iter().cloned()
.filter(|x| x.as_str().contains(nth_char(correspondence[1], 0))).count();
let segment2_count = self.input_signal.iter().cloned()
.filter(|x| x.as_str().contains(nth_char(correspondence[1], 1))).count();
let segments = match (segment1_count, segment2_count) {
(8, 9) => Ok((nth_char(correspondence[1], 0), nth_char(correspondence[1], 1))),
(9, 8) => Ok((nth_char(correspondence[1], 1), nth_char(correspondence[1], 0))),
_ => Err("Cannot identify segments")
};
- Step 3 (the most complex) : identify all the 5-segments digits, using the information gathered from step 2, and all the 6-segments digits (0, 6 and 9), by finding their “missing” segment and counting how many times this segment is used in the other digits
let general_set = "abcdefg".chars().collect::<HashSet<char>>();
for pattern in &self.input_signal {
if pattern.len() == 5 {
match (pattern.as_str().contains(segments.unwrap().0),
pattern.as_str().contains(segments.unwrap().1)) {
(true, true) => { correspondence[3] = pattern; }
(true, false) => { correspondence[2] = pattern; }
(false, true) => { correspondence[5] = pattern; }
(false, false) => { panic!(); }
}
}
if pattern.len() == 6 {
let pattern_set = pattern.as_str().chars().collect::<HashSet<char>>();
let missing = general_set.difference(&pattern_set).next().unwrap();
let missing_count = self.input_signal.iter()
.filter(|&x| x.as_str().find(|c| c == *missing).is_some()).count();
match missing_count {
4 => { correspondence[9] = pattern; }
7 => { correspondence[0] = pattern; }
8 => { correspondence[6] = pattern; }
_ => { panic!("Cannot identify segemnts"); }
}
}
}
Before returning the result, we check if all digits have been identified. If not, that would mean that there is a bug in either our code, or in the input file.
The most difficult part is done, we just have to decode the output signal with our correspondence table.
pub fn output_number(&self) -> u64 {
let mut number = 0;
let correspondence = self.correspondence_table();
for pattern in &self.output_signal {
let sorted = chars_sort(pattern.to_string());
for i in 0..10 {
if sorted == chars_sort(correspondence[i].to_string()) {
number = number * 10 + (i as u64);
break;
}
}
}
number
}
Finally, in the main function, we sum the output numbers.
pub fn solve_part2(input: &Vec<String>) -> u64 {
let mut entries_sum = 0;
for line in input {
entries_sum += Entry::new(line.to_string()).output_number();
}
entries_sum
}
I had many difficulties with Rust String methods, and I lost way too much time trying to compile the code. Then, I forgot the check described in step 3 description, and I gave a wrong answer on my first attempt, although my unit tests passed.