1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
//! This is my solution for [Advent of Code - Day 8 - _Seven Segment Search_](https://adventofcode.com/2021/day/8)
//!
//! Today had a lot of description for a relatively simple part one, that seemed to be leading into
//! a much harder part two. The puzzle input is a sequence of lines in two parts, ten unique digits
//! (i.e. 0-9) in a random order, then 4 other digits. But each digit is shown as the set of bars
//! they light up on a digital display, each bar represented by a letter a-g, in a random order.
//! To further complicate things, the wires that light up the bars have got swapped around - and the
//! actual bars that light up is only consistent for each line.
//!
//! To show some examples:
//!
//! ```text
//! Wiring correct - Order: a,b,c,d,e,f,g
//! 1: 4: 8: Sets shown when labelled in reading order
//! .... .... aaaa 1: cf, fc
//! . c b c b c 4: bcfd, cdbf, ...
//! . c b c b c 8: abcdefg, gfacbed, ...
//! .... dddd dddd
//! . f . f e f
//! . f . f e f
//! .... .... gggg
//!
//! Wiring mixed up - Order: e,c,d,g,f,a,b
//! 1: 4: 8: Example sets that could be shown when labelled in reading order
//! .... .... eeee 1: be, eb
//! c . c d c d 4: bceg, gcbe, ...
//! c . c d c d 8: abcdefg, gfacbed, ... // doesn't change all are lit up
//! .... .... gggg
//! f . f . f a
//! f . f . f a
//! .... bbbb bbbb
//! ```
//!
//! The overall puzzle seemed to be leaning towards working out what numbers were showing on each
//! line. For part one it was noted that digits 1, 4, 7 and 8 each light up a unique number of bars.
//! Due to this they can be identified by just counting the bars in the set. After all that preamble
//! the challenge for part one was simple - count the number of digits in the output strings that
//! are known to be 1, 4, 7, or 8 by their length.
//!
//! One way to solve this would be to just discard the set of ten unique combinations, reduce the
//! outputs to their sizes filter out those of size 2 (1s), size 3 (7s), size 4 (4s), and size 7
//! (8s). I felt this would be wasted work however as for part two I was going to need to identify
//! which of the digits was which. I instead decided to implement an internal representation of each
//! line as a [`Display`] that had a map of the character set to the integer digit, plus the unknown
//! digits for later analysis. To implement the minimum needed for part one, just the map entries
//! for 1, 4, 7 and 8 would need to be created. The solution for part one is to filter the output
//! lists to those that map to 1, 4, 7, or 8 in the digit map, count each of those and sum those
//! counts for all displays. [`count_unique`] implements this.
//!
//! The key piece of functionality, where all the hard work for part one and two is done is
//! [`parse_line`]. This takes the string line, and builds that map of known character sets to
//! integer digits. I'd originally implemented the digits as `HashSet<char>`s. It needs to be a type
//! where the order of the characters doesn't matter. I also suspect it needs to be able to do
//! efficient intersections of the sets, as whilst getting to grips with the specification I'd
//! already noticed you can work out 0, 6 and 9 once you know 1 and 4 because 4 is a subset of 9,
//! and 1 is a subset of 0 and 9. This hit a snag in that a `HashSet` doesn't itself implement
//! `Hash` so can't be used as a key. I pondered sorting the strings and using that as the key, but
//! the logic to calculate their intersections is pretty complex in Rust. Then I remembered
//! [`crate::day_3`] and went with representing each of the 7 lines as a bit (giving a unique number
//! for each set). Further, bitwise `&` can be used to efficiently do the intersections. I gave this
//! it's own type [`Digit`], and hooked into the built-in FromStr trait to make creating these from
//! the input cleaner. For part one only the first pass through the 10 digits was implemented, but
//! I'm happy to report that my plan worked and I did not need to change my implementation of part
//! one to cope with the changes added to solve part two.
//!
//! Part two required working out the remaining six digits, interpreting the four output digits as a
//! 4 digit decimal number, and summing those to get the puzzle solution. My implementation for part
//! one was a good start to this. As noted above, 0, 6 and 9 can be worked out from 1 and 4. So I
//! added a second pass through the unique digits to grab the three with length 6 that correspond to
//! 0, 6 and 9 in some order. These could then be tested as follows:
//! - Intersect the set of digits for #4 with the current set. If that's unchanged, #4 is a subset
//! and the current digit must be 9.
//! - Intersect the set of digits for #1 with the current set. If unchanged it's either 0 or 9, and
//! it can't be 9 as it failed the first check. It must be 0.
//! - Otherwise it's 6 by process of elimination.
//!
//! I added these to the test suite and confirmed it was all working.
//!
//! For 2, 3, and 5 a similar argument can be made, so a third loop was made just considering the
//! digits with sets of length 5.
//! - 1 is a subset of 3, but not 2 or 5 so intersect with the set for #1, if unchanged it must be 3
//! - 5 is a subset of 9, but 2 (and 3) are not. This still intersects the current set with the set
//! for #9, but instead check that the result is the same as the current set, since 5 is the
//! subset.
//! - Again we can otherwise assume the current set represents 2 by process of elimination.
//!
//! Again this was added to the test suite and implemented. All ten digits are now known, so the
//! final step was to implement [`Display::get_output`] that converted the four output digits into
//! the equivalent decimal `usize`, and I used built in iterate -> map -> sum to reduce the input
//! to the solution.
use std::collections::HashMap;
use std::fs;
use std::str::FromStr;
#[derive(Eq, PartialEq, Debug)]
struct Display {
/// Map of the sets of lines and the decimal digit they represent
digits: HashMap<usize, usize>,
/// The four output digits
output: Vec<Digit>,
}
impl Display {
/// Map each output digit to the corresponding decimal and combine by folding.
fn get_output(&self) -> usize {
self.output
.iter()
.map(|d| {
self.digits
.get(&d.bits)
.expect(format!("Missing {:?}", d).as_str())
})
.fold(0, |acc, digit| acc * 10 + digit)
}
}
#[derive(Eq, PartialEq, Debug)]
struct Digit {
/// The set of bits that are lit up with a being least significant and g being most
bits: usize,
/// The number of bits that are set. This is known at creation so cache to avoid recalculating
/// later
len: usize,
}
impl FromStr for Digit {
type Err = ();
/// Convert the string puzzle representation to a [`Digit`].
///
/// Technically this accepts more than just sets of a-g, but that does not need to be handled
/// for the puzzle input.
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut digit = Digit { bits: 0, len: 0 };
s.chars().for_each(|c| {
// chars can be converted to their ascii int just by caching - so this calculates the
// offset from 'a'
let pos = (c as usize) - ('a' as usize);
// shift left once for each digit
digit.bits = digit.bits | (1 << pos);
// track the number of bits set
digit.len = digit.len + 1;
});
Ok(digit)
}
}
/// The entry point for running the solutions with the 'real' puzzle input.
///
/// - The puzzle input is expected to be at `<project_root>/res/day-8-input`
/// - It is expected this will be called by [`super::main()`] when the user elects to run day 8.
pub fn run() {
let contents = fs::read_to_string("res/day-8-input").expect("Failed to read file");
let displays = parse_input(contents);
let unique_count = count_unique(&displays);
println!(
"There are {} digits with unique lengths in the output.",
unique_count
);
let output_total: usize = displays.iter().map(Display::get_output).sum();
println!("The sum of the outputs is {}.", output_total);
}
/// Utility for the whole puzzle input that just defers to [`parse_line`] for each line of the
/// input.
fn parse_input(input: String) -> Vec<Display> {
input.lines().map(|line| parse_line(line)).collect()
}
/// This does all of the hard work. Once the input is turned into a [`Display`] the puzzle solution
/// is easy to calculate. The string is split up by the | and ' ' delimeteters and the resulting 14
/// digits are parsed using [`Digit::from_str`]. Then there are the three loops discussed in the
/// preamble that identify 1, 4, 7 and 8; 0, 6 and 9, then finally 2, 3, and 5. Building the digits
/// map needed for [`Display`] as numbers are found.
fn parse_line(line: &str) -> Display {
// Extracted to avoid repetition, also can use more implicit typing this way.
fn parse_digit(digit: &str) -> Digit {
digit.parse().unwrap()
}
// First split into the digits and output
if let Some((digit_strings, output_strings)) = line.split_once(" | ") {
// Setup an empty map to be populated as we resolve each digit
let mut digits: HashMap<usize, usize> = HashMap::new();
// First interpret the two halves into the internal Digit representation
let unassigned_digits: Vec<Digit> = digit_strings.split(' ').map(parse_digit).collect();
let output = output_strings.split(' ').map(parse_digit).take(4).collect();
// Cache for the bit sets we'll need to isolate other digits later
let mut four: Option<usize> = None;
let mut one: Option<usize> = None;
let mut nine: Option<usize> = None;
// First pass - capture digits that have a unique length
unassigned_digits.iter().for_each(|digit| {
match digit.len {
2 => {
digits.insert(digit.bits, 1);
one = Some(digit.bits);
}
3 => {
digits.insert(digit.bits, 7);
}
4 => {
digits.insert(digit.bits, 4);
four = Some(digit.bits);
}
7 => {
digits.insert(digit.bits, 8);
}
_ => {}
};
});
// ---- Passes two and three were implemented for part two - part one stopped here ----
// Second pass - capture 6, 9, 0 using their intersection with unique digits 1 and 4
unassigned_digits
.iter()
.filter(|digit| digit.len == 6)
.for_each(|digit| {
// 9 intersects with 4, 6 and 0 don't.
if digit.bits & four.expect("digits missing 4") == four.unwrap() {
digits.insert(digit.bits, 9);
nine = Some(digit.bits);
}
// 0 and 9 intersect with 1, but 9 is already captured above
else if digit.bits & one.expect("digits missing 1") == one.unwrap() {
digits.insert(digit.bits, 0);
}
// Can only be 6 by process of elimination
else {
digits.insert(digit.bits, 6);
}
});
// Third pass - capture 2, 3, 5 using their intersections with digits 1 and 9
unassigned_digits
.iter()
.filter(|digit| digit.len == 5)
.for_each(|digit| {
// 1 is included in 3, but not 2 or 5
if digit.bits & one.expect("digits missing 1") == one.unwrap() {
digits.insert(digit.bits, 3);
}
// 5 is included in 9, but not in 2 and 3 are not
else if digit.bits & nine.expect("digits missing 9") == digit.bits {
digits.insert(digit.bits, 5);
}
// Can only be 2 by process of elimination
else {
digits.insert(digit.bits, 2);
}
});
return Display { digits, output };
}
// Failed to match two sections split by | - should be unreachable for the puzzle input, so
// just panic!
panic!("Bad line: '{}'", line)
}
/// Given a list of parsed displays, count the total number of 1s, 4s, 7s, and 8s in their outputs
fn count_unique(displays: &Vec<Display>) -> usize {
displays
.iter()
.map(|display| {
return display
.output
.iter()
// Flat map converts to the digit and filters out any unmatched digits to support
// the part one implementation did not include a total mapping for each digit
.flat_map(|digit| display.digits.get(&digit.bits))
// Limit to just the four digits that part one cares about so that this still
// works when part two is implemented.
.filter(|digit| [1, 4, 7, 8].contains(digit))
// Just need to know how many match
.count();
})
.sum()
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::str::FromStr;
use crate::day_8::{count_unique, parse_input, parse_line, Digit, Display};
#[test]
fn can_parse_digit() {
assert_eq!(
Digit::from_str("acedgfb"),
Ok(Digit {
bits: 0b1111111,
len: 7
})
);
assert_eq!(
Digit::from_str("cdfbe"),
Ok(Digit {
bits: 0b0111110,
len: 5
})
);
assert_eq!(
Digit::from_str("gcdfa"),
Ok(Digit {
bits: 0b1101101,
len: 5
})
);
assert_eq!(
Digit::from_str("fbcad"),
Ok(Digit {
bits: 0b0101111,
len: 5
})
);
assert_eq!(
Digit::from_str("dab"),
Ok(Digit {
bits: 0b0001011,
len: 3
})
);
assert_eq!(
Digit::from_str("cefabd"),
Ok(Digit {
bits: 0b0111111,
len: 6
})
);
assert_eq!(
Digit::from_str("cdfgeb"),
Ok(Digit {
bits: 0b1111110,
len: 6
})
);
assert_eq!(
Digit::from_str("eafb"),
Ok(Digit {
bits: 0b0110011,
len: 4
})
);
assert_eq!(
Digit::from_str("cagedb"),
Ok(Digit {
bits: 0b1011111,
len: 6
})
);
assert_eq!(
Digit::from_str("ab"),
Ok(Digit {
bits: 0b000011,
len: 2
})
);
}
#[test]
fn can_parse_lines() {
let display = parse_line(get_sample_line());
let digits = HashMap::from([
(Digit::from_str("cagedb").unwrap().bits, 0usize),
(Digit::from_str("ab").unwrap().bits, 1usize),
(Digit::from_str("gcdfa").unwrap().bits, 2usize),
(Digit::from_str("fbcad").unwrap().bits, 3usize),
(Digit::from_str("eafb").unwrap().bits, 4usize),
(Digit::from_str("cdfbe").unwrap().bits, 5usize),
(Digit::from_str("cdfgeb").unwrap().bits, 6usize),
(Digit::from_str("dab").unwrap().bits, 7usize),
(Digit::from_str("acedgfb").unwrap().bits, 8usize),
(Digit::from_str("cefabd").unwrap().bits, 9usize),
]);
let output = vec![
Digit::from_str("cdfeb").unwrap(),
Digit::from_str("fcadb").unwrap(),
Digit::from_str("cdfeb").unwrap(),
Digit::from_str("cdbaf").unwrap(),
];
assert_eq!(display, Display { digits, output });
}
#[test]
fn can_calculate_output() {
assert_eq!(parse_line(get_sample_line()).get_output(), 5353);
let expected_outputs: Vec<usize> =
vec![8394, 9781, 1197, 9361, 4873, 8418, 4548, 1625, 8717, 4315];
parse_input(get_sample_input())
.iter()
.zip(expected_outputs)
.for_each(|(display, expected_output)| {
assert_eq!(display.get_output(), expected_output)
})
}
fn get_sample_line() -> &'static str {
"acedgfb cdfbe gcdfa fbcad dab cefabd cdfgeb eafb cagedb ab | cdfeb fcadb cdfeb cdbaf"
}
#[test]
fn can_count_unique() {
let displays: Vec<Display> = parse_input(get_sample_input());
assert_eq!(count_unique(&displays), 26);
}
fn get_sample_input() -> String {
"be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe
edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc
fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg
fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb
aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea
fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb
dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe
bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef
egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb
gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce"
.to_string()
}
}
