11 votes

Day 8: Seven Segment Search

Today's problem description: https://adventofcode.com/2021/day/8

Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):

<details>
<summary>Part 1</summary>

```python
Your code here.
```

</details>

22 comments

  1. [2]
    Liru
    Link
    Part 1 was trivial once you notice that the inputs are aligned. Part 1, Unix cut -c 60- 08.txt | grep -o "\b\([a-g]\{2,4\}\|[a-g]\{7\}\)\b" | wc -l I can provide commentary if anyone is interested...

    Part 1 was trivial once you notice that the inputs are aligned.

    Part 1, Unix
    cut -c 60- 08.txt | grep -o "\b\([a-g]\{2,4\}\|[a-g]\{7\}\)\b" | wc -l
    
    I can provide commentary if anyone is interested but it feels roughly understandable.

    Still working on making part 2 in Rust look nice, but it's pretty damn ugly.

    7 votes
    1. tomf
      Link Parent
      that's beautiful!

      that's beautiful!

      3 votes
  2. tjf
    (edited )
    Link
    Python golf, day 8. Tips/improvements always welcome. Part 1 print(sum([len(d)in(2,4,3,7)for l in open(0)for d in l.split('|')[1].split()])) Here I converted strings to 7-bit binary numbers (e.g....

    Python golf, day 8. Tips/improvements always welcome.

    Part 1
    print(sum([len(d)in(2,4,3,7)for l in open(0)for d in l.split('|')[1].split()]))
    

    Here I converted strings to 7-bit binary numbers (e.g. 'abc' -> 1110000, 'cf' -> 0010010) then did some bit twiddling.
    This could probably be golfed better but I need to get on with my day.

    Part 2
    s=0
    p=lambda h:[''.join(d)for d in h.split()]
    b=lambda x:sum(2**'gfedcba'.index(y)for y in x)
    e=lambda n:filter(lambda d:len(d)==n,l)
    for I in open(0):l,r=map(p,I.split('|'));m={};o,f=[b([*e(w)][0])for w in (2,4)];[m.update({b(d):6 if o&b(d)-o else(0 if f&b(d)-f else 9)})for d in e(6)];[m.update({b(d):3 if o&b(d)==o else(2 if o|f|b(d)==127 else 5)})for d in e(5)];s+=sum([10**i*{2:1,3:7,4:4,7:8}.get(len(d),m.get(b(d)))for i,d in enumerate(r[::-1])])
    print(s)
    

    Edit: using sorted() in part 2 was unnecessary, so I removed it and saved some bytes!

    7 votes
  3. [2]
    spit-evil-olive-tips
    (edited )
    Link
    Part 1 the key trick here is that I canonicalize both the patterns and the outputs by sorting them character-wise using ''.join(sorted(some_string)) then comparing output digits to seeing if they...
    Part 1

    the key trick here is that I canonicalize both the patterns and the outputs by sorting them character-wise using ''.join(sorted(some_string))

    then comparing output digits to seeing if they match any of the 1, 4, 7, 8 patterns can be done as a simple string comparison of the canonical strings.

    def main():
        with open('008.txt') as file:
            lines = file.readlines()
    
        count = 0
        for line in lines:
            count += process_line(line.strip())
    
        print(count)
    
    def process_line(line):
        patterns, outputs = line.split(' | ')
        patterns = [''.join(sorted(pattern)) for pattern in patterns.split(' ')]
        outputs = [''.join(sorted(output)) for output in outputs.split(' ')]
    
        one_pattern = next(p for p in patterns if len(p) == 2)
        four_pattern = next(p for p in patterns if len(p) == 4)
        seven_pattern = next(p for p in patterns if len(p) == 3)
        eight_pattern = next(p for p in patterns if len(p) == 7)
    
        count = 0
        for output in outputs:
            if output in (one_pattern, four_pattern, seven_pattern, eight_pattern):
                count += 1
    
        return count
    
    main()
    
    Part 2

    this was fun.

    instead of character-sorted strings, I'm now representing the patterns as sets of individual characters. then set operations can be used to do comparisons of segments.

    I added some inline comments explaining the thought process / rationale going into each deduction

    def main():
        with open('008.txt') as file:
            lines = file.readlines()
    
        total = 0
        for line in lines:
            total += process_line(line.strip())
    
        print(total)
    
    
    def process_line(line):
        patterns, outputs = line.split(' | ')
        patterns = [set(pattern) for pattern in patterns.split(' ')]
        outputs = [set(output) for output in outputs.split(' ')]
    
        # mapping from digit -> pattern for that digit
        mappings = dict()
    
        # reproducing the chart from the website:
    
        #   0:      1:      2:      3:      4:
        #  aaaa    ....    aaaa    aaaa    ....
        # b    c  .    c  .    c  .    c  b    c
        # b    c  .    c  .    c  .    c  b    c
        #  ....    ....    dddd    dddd    dddd
        # e    f  .    f  e    .  .    f  .    f
        # e    f  .    f  e    .  .    f  .    f
        #  gggg    ....    gggg    gggg    ....
        #
        #   5:      6:      7:      8:      9:
        #  aaaa    aaaa    aaaa    aaaa    aaaa
        # b    .  b    .  .    c  b    c  b    c
        # b    .  b    .  .    c  b    c  b    c
        #  dddd    dddd    ....    dddd    dddd
        # .    f  e    f  .    f  e    f  .    f
        # .    f  e    f  .    f  e    f  .    f
        #  gggg    gggg    ....    gggg    gggg
    
        # we can uniquely identify the digits 1, 4, 7, 8 because their length
        # (the number of segments they light up) is unique
        mappings[1] = next(p for p in patterns if len(p) == 2)
        mappings[4] = next(p for p in patterns if len(p) == 4)
        mappings[7] = next(p for p in patterns if len(p) == 3)
        mappings[8] = next(p for p in patterns if len(p) == 7)
    
        # of the remaining digits, three (2, 3, 5) have a length / segment count
        # of five
        fives = [p for p in patterns if len(p) == 5]
        # while three more (0, 6, 9) have a segment count of six
        sixes = [p for p in patterns if len(p) == 6]
    
        # if you compare 2, 3, and 5, 3 is the only one that has both C and F lit
        # up. and we know the C and F segments, that's the pattern for 1, so we
        # can deduce the pattern for 3 is the only one of these length-5 patterns
        # that contains the pattern for 1
        mappings[3] = next(p for p in fives if mappings[1].issubset(p))
    
        # we can isolate the B-D segment by looking at the segments lit up by 4 but
        # not by 1
        bd_segment = mappings[4] - mappings[1]
    
        # we can identify 5 because it has the B-D segment lit up and 2 and 3 don't
        mappings[5] = next(p for p in fives if bd_segment.issubset(p))
    
        # then we can conclude the pattern for 2 is the only one of the length-5
        # patterns left
        mappings[2] = next(p for p in fives if p not in (mappings[3], mappings[5]))
    
        # next, we look at the three length-6 patterns, and do the same thing of
        # trying to discriminate between them.
    
        # of 0, 6, and 9, both 6 and 9 have the B-D segment lit, 0 does not
        mappings[0] = next(p for p in sixes if not bd_segment.issubset(p))
    
        # 6 is the only one of the three that has the C-F segment (the pattern for
        # 1) not lit
        mappings[6] = next(p for p in sixes if not mappings[1].issubset(p))
    
        # and again, process of elimination, 9 is the one that's left
        mappings[9] = next(p for p in sixes if p not in (mappings[0], mappings[6]))
    
        # then we reverse the mapping we just built, so it's an index from the
        # segments to their numeric value
        #
        # implementation note: set in python is not hashable, so it cannot serve
        # as a dictionary key. we could use frozenset, but it's a little simpler
        # to just use their canonical string representation as the key
        digit_mappings = {
            ''.join(sorted(pattern)): value
            for value, pattern in mappings.items()
        }
    
        # then decode those digits and convert to base 10
        value = 0
        for i, output in enumerate(reversed(outputs)):
            output = ''.join(sorted(output))
            digit = digit_mappings[output]
            value += digit * 10**i
    
        return value
    
    main()
    
    6 votes
    1. asterisk
      Link Parent
      Neat! I also thought about set but I dunno why my dumb mind was just repeating me «keep strings sorted, keep…».

      Neat! I also thought about set but I dunno why my dumb mind was just repeating me «keep strings sorted, keep…».

      1 vote
  4. PapaNachos
    Link
    I freaked out a bit when I read the problem description, especially for part B. But after working at it for a bit I was able to chip away at it and eventually get to a solution. Day 8 Part A -...

    I freaked out a bit when I read the problem description, especially for part B. But after working at it for a bit I was able to chip away at it and eventually get to a solution.

    Day 8 Part A - Python

    This one wasn't too bad. I ended up using regular expressions to ID the lettering segments based on their length.

    import re
    
    #data = test_data
    data = real_data
    data = data.split('\n')
    
    output_data = []
    for line in data:
        output_data.append(line.split('|')[1])
    
    #print(output_data)
    one_pattern = re.compile(r'\b\w{2}\b')
    four_pattern = re.compile(r'\b\w{4}\b')
    seven_pattern = re.compile(r'\b\w{3}\b')
    eight_pattern = re.compile(r'\b\w{7}\b')
    
    
    ones_count = 0
    fours_count = 0
    sevens_count = 0
    eights_count = 0
    for row in output_data:
        ones_count = ones_count + len(re.findall(one_pattern, row))
        fours_count = fours_count + len(re.findall(four_pattern, row))
        sevens_count = sevens_count + len(re.findall(seven_pattern, row))
        eights_count = eights_count + len(re.findall(eight_pattern, row))
    
    print(ones_count + fours_count + sevens_count + eights_count)
    
    Day 8 Part B - Python

    Holy fuck that escalated. Using the segments I knew from the previous portion, I was able to slowly figure out which letter combinations were which symbols. Once I had all the different letter combinations identified, I built a quick decoder and got my answer. And holy fuck, sets came in handy for this part. They were today's MVP.

    import re
    
    #data = test_data
    data = real_data
    data = data.split('\n')
    
    #new_test_data = '''acedgfb cdfbe gcdfa fbcad dab cefabd cdfgeb eafb cagedb ab | cdfeb fcadb cdfeb cdbaf'''
    
    #data = new_test_data.split('\n')
    
    #print(output_data)
    one_pattern = re.compile(r'\b\w{2}\b')
    four_pattern = re.compile(r'\b\w{4}\b')
    seven_pattern = re.compile(r'\b\w{3}\b')
    eight_pattern = re.compile(r'\b\w{7}\b')
    five_pattern = re.compile(r'\b\w{5}\b')
    six_pattern = re.compile(r'\b\w{6}\b')
    
    output_sum = 0
    
    for row in data:
        signal_pattern, output = row.split('|')
        one_letters = list(re.findall(one_pattern,signal_pattern)[0])
        four_letters = list(re.findall(four_pattern,signal_pattern)[0])
        seven_letters = list(re.findall(seven_pattern,signal_pattern)[0])
        eight_letters = list(re.findall(eight_pattern,signal_pattern)[0])
        five_words = re.findall(five_pattern, signal_pattern)
        six_words = re.findall(six_pattern, signal_pattern)
        
        #Uses the difference between 7 and 1 to find the top segment
        top_segment = set(seven_letters) - set(one_letters)
        
        #five_words
        two_letters = []
        three_letters = []
        five_letters = []
        
        #six_words
        zero_letters = []
        six_letters = []
        five_letters = []
        
        #find 3
        for index, val in enumerate(five_words):
            if len(set(list(val)) - set(one_letters)) == 3:
                three_letters = five_words[index]
        five_words.remove(three_letters)
        
        #find 6
        for index, val in enumerate(six_words):
            if len(set(list(val)) - set(one_letters)) == 5:
                six_letters = six_words[index]
        six_words.remove(six_letters)
        
        #ID the missing segment of 6 to find the top right segment
        top_right_segment = set(eight_letters) - set(six_letters)
        
        #use the top right segment to find 2
        for index, val in enumerate(five_words):
            if len(set(list(val)) - top_right_segment) == 4:
                two_letters = five_words[index]
        five_words.remove(two_letters)
        #process of elimination
        five_letters = five_words[0]
        
        #find 6
        for index, val in enumerate(six_words):
            if len(set(list(val)) - set(four_letters)) == 2:
                nine_letters = six_words[index]
        six_words.remove(nine_letters)
    
        #process of elimination
        zero_letters = six_words[0]
        
        def decode(display):
            display_letters = list(display)
            if set(display_letters) == set(zero_letters): return "0"
            if set(display_letters) == set(one_letters): return "1"
            if set(display_letters) == set(two_letters): return "2"
            if set(display_letters) == set(three_letters): return "3"
            if set(display_letters) == set(four_letters): return "4"
            if set(display_letters) == set(five_letters): return "5"
            if set(display_letters) == set(six_letters): return "6"
            if set(display_letters) == set(seven_letters): return "7"
            if set(display_letters) == set(eight_letters): return "8"
            if set(display_letters) == set(nine_letters): return "9"
            
        output_string = ""
        for segment in output.split():
            output_string = output_string + decode(segment)
        output_sum = output_sum + int(output_string)
        
    print(output_sum)
    
    Tips
    • It's a big problem, you might need to break it down into smaller portions and take it on one segment at a time (no pun intended)

    • Python's sets were extremely helpful for me, since they let you compare similar or different elements within groups of letters regardless of order. If your language has something similar, it may be helpful

    • Try drawing out each of the numbers with which segments are active and look for differences and similarities between them. Start with what you know and work from there.

    • The '9' option has 6 segments active. Not 5.

    5 votes
  5. tomf
    (edited )
    Link
    part 2? no thanks! I was close a few times, but just not there. Part 1 was a breeze. G2:G has a list of the segments Part 1 ``` =ARRAYFORMULA( IF(C3=FALSE,, QUERY( QUERY( FLATTEN( LEN( IFERROR(...

    part 2? no thanks! I was close a few times, but just not there.

    Part 1 was a breeze. G2:G has a list of the segments

    Part 1 ``` =ARRAYFORMULA( IF(C3=FALSE,, QUERY( QUERY( FLATTEN( LEN( IFERROR( SPLIT( IF(ISBLANK(A2:A),, INDEX( TRIM( SPLIT(A2:A,"|")), 0,2)), " ")))), "select Col1, Count(Col1) where Col1 > 0 group by Col1 label Count(Col1) ''"), "select Sum(Col2) where Col1 matches '"&JOIN("|",FILTER(LEN(G2:G11),COUNTIF(LEN(G2:G11),LEN(G2:G11))=1))&"' label Sum(Col2) ''"))) ```

    edit: I got part 2. Its brutal. You can check it out here

    5 votes
  6. [2]
    wycy
    (edited )
    Link
    Rust Brutal. SPOILERI spent so much time trying to figure out the individual wire mapping before realizing I didn't need to. Deleted 75% of my code in the end. Rust use std::env; use...

    Rust

    Brutal.

    SPOILERI spent so much time trying to figure out the individual wire mapping before realizing I didn't need to. Deleted 75% of my code in the end.

    Rust
    use std::env;
    use std::io::{self, prelude::*, BufReader};
    use std::fs::File;
    
    // Count number of matches second string has to characters in first string
    fn num_matches(first: &String, second: &String) -> usize {
        first.chars().filter(|c| second.contains(&c.to_string())).count()
    }
    
    struct PuzzleInput {
        signals: Vec<String>,
        outputs: Vec<String>,
    }
    impl PuzzleInput {
        pub fn output(&self) -> usize {
            let signal1 = self.signals.iter().filter(|s| s.len() == 2).map(|s| s.to_string()).next().unwrap();
            let signal4 = self.signals.iter().filter(|s| s.len() == 4).map(|s| s.to_string()).next().unwrap();
          //let signal7 = self.signals.iter().filter(|s| s.len() == 3).map(|s| s.to_string()).next().unwrap();
          //let signal8 = self.signals.iter().filter(|s| s.len() == 7).map(|s| s.to_string()).next().unwrap();
    
            let mut output = String::new();
            for o in &self.outputs {
                match (o.len(),num_matches(&o,&signal1),num_matches(&o,&signal4)) {
                    (6,2,3) => output.push_str("0"),
                    (2,_,_) => output.push_str("1"),
                    (5,1,2) => output.push_str("2"),
                    (5,2,3) => output.push_str("3"),
                    (4,_,_) => output.push_str("4"),
                    (5,1,3) => output.push_str("5"),
                    (6,1,3) => output.push_str("6"),
                    (3,_,_) => output.push_str("7"),
                    (7,_,_) => output.push_str("8"),
                    (6,_,4) => output.push_str("9"),
                    (_,_,_) => panic!("Unknown failure for signal: {}",&o),
                }
            }
            output.parse::<usize>().unwrap()
        }
    }
    impl From<&String> for PuzzleInput {
        fn from(s: &String) -> Self {
            let parts = s.split(" | ").collect::<Vec<_>>();
            let signals = parts[0].split_ascii_whitespace().map(|x| x.to_string()).collect();
            let outputs = parts[1].split_ascii_whitespace().map(|x| x.to_string()).collect();
            Self {
                signals: signals,
                outputs: outputs,
            }
        }
    }
    
    fn day08(input: &str) -> io::Result<()> {
        let file = File::open(input).expect("Input file not found.");
        let reader = BufReader::new(file);
    
        // Input
        let input: Vec<String> = match reader.lines().collect() {
            Err(err) => panic!("Unknown error reading input: {}", err),
            Ok(result) => result,
        };
        let inputs: Vec<_> = input.iter().map(PuzzleInput::from).collect();
    
        // Part 1
        let part1 = inputs
            .iter()
            .map(|x| {
                x.outputs
                    .iter()
                    .filter(|o| o.len() == 2 || o.len() == 4 || o.len() == 3 || o.len() == 7) // 1,4,7,8
                    .count()
            })
            .sum::<usize>();
        println!("Part 1: {}", part1); // 488
    
        // Part 2
        let part2 = inputs.iter().map(|x| x.output()).sum::<usize>();
        println!("Part 2: {}", part2); // 1040429
    
        Ok(())
    }
    
    fn main() {
        let args: Vec<String> = env::args().collect();
        let filename = &args[1];
        day08(&filename).unwrap();
    }
    
    5 votes
    1. jzimbel
      Link Parent
      Matching against how many segments are shared between the signal in question and a couple of the known ones is so smart! (And concise!)

      Matching against how many segments are shared between the signal in question and a couple of the known ones is so smart! (And concise!)

      2 votes
  7. Crestwave
    (edited )
    Link
    This was interesting. Part 1 was pretty simple, although there's quite a bit of info to take in. Part 2 was very open-ended, I ended up commenting my code for once in case anyone is curious on my...

    This was interesting. Part 1 was pretty simple, although there's quite a bit of info to take in.
    Part 2 was very open-ended, I ended up commenting my code for once in case anyone is curious on my solution. It's a bit of a process but I'm quite happy with it overall.

    Part 1
    #!/usr/bin/awk -f
    {
    	split($0, input, " \\| ")
    	split(input[2], output, " ")
    
    	for (i in output)
    		if (length(output[i]) ~ /^[2437]$/)
    			total += 1
    }
    
    END {
    	print total
    }
    
    Part 2 I deemed comparisons too messy for POSIX AWK, so my solution hinges on the number of occurrences of a segment:
    a 8
    b 6
    c 8
    d 7
    e 4
    f 9
    g 7
    

    The number 4 has segments c and d and not a or g, meaning that we can get those out of the way and map all the letters in a single clean pass.

    #!/usr/bin/awk -f
    {
    	# Parse input
    	split($0, input, " \\| ")
    	split(input[1], signal, " ")
    
    	# Clear the arrays since AWK lacks local variables
    	# `delete arr` is not POSIX-compatible
    	split("", trans)
    	split("", count)
    
    	# Count the number of occurences for each letter,
    	# and get the pattern of number 4
    	for (i in signal) {
    		split(signal[i], chars, "")
    
    		for (c in chars)
    			count[chars[c]] += 1
    
    		if (length(signal[i]) == 4)
    			map = signal[i]
    	}
    
    	split(map, chars, "")
    
    	# With 4's pattern, we can distinguish between the
    	# 2 letters that have 8 characters, as well as the
    	# 2 letters that have 7 characters.
    	for (c in chars) {
    		if (count[chars[c]] == 8) {
    			trans[chars[c]] = "C"
    			delete count[chars[c]]
    		} else if (count[chars[c]] == 7) {
    			trans[chars[c]] = "D"
    			delete count[chars[c]]
    		}
    	}
    
    	# Now that we no longer have any duplicate counts,
    	# map the remaining letters to the translation array,
    	for (i in count) {
    		if (count[i] == 8)
    			trans[i] = "A"
    		else if (count[i] == 6)
    			trans[i] = "B"
    		else if (count[i] == 7)
    			trans[i] = "G"
    		else if (count[i] == 4)
    			trans[i] = "E"
    		else if (count[i] == 9)
    			trans[i] = "F"
    	}
    
    	# Translate the output values
    	for (c in trans)
    		gsub(c, trans[c], input[2])
    
    	split(input[2], output, " ")
    	val = ""
    
    	# Now that we have the correct segments
    	# (e.g., GADBF for 5), we can translate it
    	# to the actual number and build the value.
    	for (i in output) {
    		# Using regex because the segments aren't sorted and
    		# POSIX AWK does not have a sort function or sets.
    		if (output[i] ~ /^[ABCEFG]{6,}$/)
    			val = val 0
    		else if (output[i] ~ /^[CF]{2,}$/)
    			val = val 1
    		else if (output[i] ~ /^[ACDEG]{5,}$/)
    			val = val 2
    		else if (output[i] ~ /^[ACDFG]{5,}$/)
    			val = val 3
    		else if (output[i] ~ /^[BCDF]{4,}$/)
    			val = val 4
    		else if (output[i] ~ /^[ABDFG]{5,}$/)
    			val = val 5
    		else if (output[i] ~ /^[ABDEFG]{6,}$/)
    			val = val 6
    		else if (output[i] ~ /^[ACF]{3,}$/)
    			val = val 7
    		else if (output[i] ~ /^[ABCDEFG]{7,}$/)
    			val = val 8
    		else if (output[i] ~ /^[ABCDFG]{6,}$/)
    			val = val 9
    	}
    
    	total += int(val)
    }
    
    END {
    	print total
    }
    
    4 votes
  8. pnutzh4x0r
    Link
    JavaScript Repo Link This one (well, mostly Part 2) made my head hurt. I wrote my first version in Python and then translated into JavaScript. Part 1 First part was straightforward, just compute...

    JavaScript

    Repo Link

    This one (well, mostly Part 2) made my head hurt. I wrote my first version in Python and then translated into JavaScript.

    Part 1

    First part was straightforward, just compute the length of each output string and check it against the known lengths.

    const KNOWN_LENGTHS = [2, 4, 3, 7];
    
    let entries = IO.readlines(line => line.split(' | '));
    let part_a  = entries.reduce((total, entry) => {
        let [_, outputs] = entry;
        outputs = outputs.split(' ');
        matches = outputs.filter(output => KNOWN_LENGTHS.includes(output.length)).length;
        return total + matches;
    }, 0);
    
    IO.print(`Part A: ${part_a}`);
    
    Part 2

    This part stumped me for a while, but I eventually saw the need to use sets for the patterns. In Python, sets are well supported and include many useful operations such as &, |, -. While JavaScript has a set, it lacks this operators, which makes them not very useful. Because of this, I decided to use integer bitsets in JavaScript with bitwise logical operators.

    function ctoi(c) {  // Character to ASCII integer
        return c.charCodeAt(0);
    }
    
    function stoi(s) {  // Signal string to integer bitset
        return s.split('').reduce((t, c) => t + (1<<(ctoi(c) - ctoi('a'))), 0);
    }
    
    let entries = IO.readlines(line => line.split(' | '))
    let part_b  = entries.reduce((total, entry) => {
        let [patterns, outputs] = entry;
        let bitsets = new Array(10).fill(0);
        patterns    = patterns.split(' ');
        outputs     = outputs.split(' ').map(stoi);
    
        // Base sets
        bitsets[1] = stoi(patterns.filter(pattern => pattern.length == 2)[0]);
        bitsets[4] = stoi(patterns.filter(pattern => pattern.length == 4)[0]);
        bitsets[7] = stoi(patterns.filter(pattern => pattern.length == 3)[0]);
        bitsets[8] = stoi(patterns.filter(pattern => pattern.length == 7)[0]);
    
        // Sets of length 5: 2, 3, 5
        let fives  = patterns.filter(pattern => pattern.length == 5).map(stoi);
        bitsets[2] = fives.filter(bitset => (bitset | bitsets[4]) == bitsets[8])[0];
        bitsets[5] = fives.filter(bitset => (bitset | bitsets[2]) == bitsets[8])[0];
        bitsets[3] = fives.filter(bitset => (bitset != bitsets[2] && bitset != bitsets[5]));
        
        // Sets of length 6: 0, 6, 9
        let sixes  = patterns.filter(pattern => pattern.length == 6).map(stoi);
        bitsets[0] = sixes.filter(bitset => (bitset | bitsets[5]) == bitsets[8])[0];
        bitsets[6] = sixes.filter(bitset => (bitset | bitsets[7]) == bitsets[8])[0];
        bitsets[9] = sixes.filter(bitset => (bitset != bitsets[0] && bitset != bitsets[6]));
    
        // Construct mapping
        let mapping = bitsets.reduce((d, b, i) => (d[b] = i, d), {});
    
        // Construct digit
        let display = outputs.reduce((total, output) => total + mapping[output].toString(), '');
    
        // Reduction
        return total + parseInt(display, 10);
    }, 0);
    
    IO.print(`Part B: ${part_b}`);
    
    4 votes
  9. DataWraith
    Link
    God. This took all day... but I finally managed to do it. (Rust) Setup and Data structures #![feature(drain_filter)] use std::collections::{HashMap, HashSet}; use nom::{ bytes::complete::tag,...

    God. This took all day... but I finally managed to do it. (Rust)

    Setup and Data structures
    #![feature(drain_filter)]
    
    use std::collections::{HashMap, HashSet};
    
    use nom::{
        bytes::complete::tag, character::complete::multispace0, character::complete::space0, IResult,
    };
    
    type Pattern = Vec<char>;
    
    #[derive(Debug, Clone)]
    struct Entry {
        candidates: Vec<Candidate>,
        output: Vec<Pattern>,
    }
    
    #[derive(Debug, Clone)]
    struct Candidate {
        pattern: Pattern,
        candidates: Vec<u8>,
    }
    
    #[derive(Debug, Clone)]
    struct Puzzle {
        entries: Vec<Entry>,
    }
    
    Input parsing
    fn parse_pattern(input: &str) -> IResult<&str, Pattern> {
        let (input, _) = space0(input)?;
        let (input, pattern) = nom::multi::many1(nom::character::complete::alpha1)(input)?;
    
        let mut sorted = pattern
            .iter()
            .flat_map(|s| s.chars().collect::<Vec<char>>())
            .collect::<Vec<char>>();
    
        sorted.sort_unstable();
    
        Ok((input, sorted))
    }
    
    fn parse_patterns(input: &str) -> IResult<&str, Vec<Pattern>> {
        nom::multi::many1(parse_pattern)(input)
    }
    
    fn parse_entry(input: &str) -> IResult<&str, Entry> {
        let (input, patterns) = parse_patterns(input)?;
        let (input, _) = space0(input)?;
        let (input, _) = tag("|")(input)?;
        let (input, output) = parse_patterns(input)?;
        let (input, _) = multispace0(input)?;
    
        let candidates: Vec<Candidate> = patterns
            .iter()
            .map(|p| Candidate {
                pattern: p.to_vec(),
                candidates: (0..=9).collect(),
            })
            .collect();
    
        Ok((input, Entry { candidates, output }))
    }
    
    fn parse_puzzle(input: &str) -> IResult<&str, Vec<Entry>> {
        let (input, entries) = nom::multi::many1(parse_entry)(input)?;
        let (input, _) = nom::combinator::eof(input)?;
    
        Ok((input, entries))
    }
    
    Helper functions
    // Sets the initial list of candidates based on the length of the pattern.
    fn number_candidates(c: &mut Candidate) {
        match c.pattern.len() {
            2 => c.candidates = vec![1],
            3 => c.candidates = vec![7],
            4 => c.candidates = vec![4],
            5 => c.candidates = vec![2, 3, 5],
            6 => c.candidates = vec![0, 6, 9],
            7 => c.candidates = vec![8],
            _ => (),
        };
    }
    
    // Returns whether an entry is a valid decoding. We check this by making sure that all numbers,
    // whose segments are a subset of another number (e.g. 7's segments are a subset of 3's segments),
    // retain that property in the chosen decoding. Except 8. Nobody likes 8.
    fn is_valid(e: &Entry) -> bool {
        let subsets = [
            (1, 0),
            (1, 3),
            (1, 4),
            (1, 7),
            (1, 9),
            (3, 9),
            (5, 6),
            (5, 9),
            (7, 0),
            (7, 3),
            (7, 9),
        ];
    
        let mut hsets: Vec<HashSet<char>> = vec![HashSet::new(); 10];
    
        for i in 0..e.candidates.len() {
            for &pc in e.candidates[i].pattern.iter() {
                hsets[e.candidates[i].candidates[0] as usize].insert(pc);
            }
        }
    
        for &(a, b) in subsets.iter() {
            if hsets[a]
                .intersection(&hsets[b])
                .cloned()
                .collect::<Vec<char>>()
                .len()
                < hsets[a].len()
            {
                return false;
            }
        }
    
        true
    }
    
    Solving
    fn solve2(e: Entry, idx: usize) -> Option<Entry> {
        if idx >= e.candidates.len() {
            // If the entry is complete, check if it is valid, and return it if so.
            if is_valid(&e) {
                return Some(e);
            }
    
            return None;
        }
    
        // Otherwise, if we have a single candidate, use that.
        if e.candidates[idx].candidates.len() == 1 {
            return solve2(e, idx + 1);
        }
    
        // Loop through all candidates and look for a valid decoding.
        for assignment in &e.candidates[idx].candidates {
            let mut ce = e.clone();
    
            ce.candidates[idx].candidates = vec![*assignment];
    
            // Remove assignment from all other candidate lists
            for (cidx, cec) in ce.candidates.iter_mut().enumerate() {
                if cidx == idx {
                    continue;
                }
    
                cec.candidates.drain_filter(|c| c == assignment);
            }
    
            let r = solve2(ce, idx + 1);
    
            if r.is_some() {
                return r;
            }
        }
    
        None
    }
    
    fn part1(puzzle: &mut Puzzle) -> usize {
        // Initial candidate assignment
        for entry in puzzle.entries.iter_mut() {
            for c in entry.candidates.iter_mut() {
                number_candidates(c);
            }
        }
    
        let mut sum = 0;
    
        // Sum up all output patterns that have exactly one candidate
        for entry in puzzle.entries.iter() {
            let mut t = HashMap::new();
    
            for c in entry.candidates.iter() {
                if c.candidates.len() == 1 {
                    t.insert(c.pattern.clone(), c.candidates[0]);
                }
            }
    
            for output in entry.output.iter() {
                if t.contains_key(output) {
                    sum += 1;
                }
            }
        }
    
        sum
    }
    
    fn part2(puzzle: &mut Puzzle) -> usize {
        // Initial candidate assignment
        for entry in puzzle.entries.iter_mut() {
            for c in entry.candidates.iter_mut() {
                number_candidates(c);
            }
        }
    
        let mut sum = 0;
        let base: usize = 10;
    
        for entry in puzzle.entries.iter_mut() {
            let entry2 = solve2(entry.clone(), 0);
            let entry2 = entry2.unwrap();
    
            let mut t = HashMap::new();
            for c in entry2.candidates.iter() {
                t.insert(c.pattern.clone(), c.candidates[0]);
            }
    
            // Read out the output numbers
            for (idx, output) in entry2.output.iter().enumerate() {
                let digit = t.get(output).unwrap();
                sum += base.pow(3 - idx as u32) * (*digit as usize);
            }
        }
    
        sum
    }
    
    fn main() {
        let input = include_str!("../../input-08.txt");
        let entries = parse_puzzle(input).expect("Failed to parse puzzle").1;
        let mut puzzle = Puzzle {
            entries: entries.clone(),
        };
    
        println!("Part I:  {}", part1(&mut puzzle));
        println!("Part II: {}", part2(&mut puzzle));
    }
    

    The code could probably be nicer, but I spent enough time on this today. :(

    4 votes
  10. asterisk
    Link
    Python import re data = list() with open("input.txt") as file: for line in file: entry = ["".join(sorted(i)) for i in re.findall(r"\w+", line)] data.append((entry[:10], entry[10:])) # tttt # l r #...
    Python
    import re
    
    data = list()
    
    with open("input.txt") as file:
        for line in file:
            entry = ["".join(sorted(i)) for i in re.findall(r"\w+", line)]
            data.append((entry[:10], entry[10:]))
    
    #  tttt
    # l    r
    # t    t
    #  mmmm
    # l    r
    # b    b
    #  bbbb
    
    count = 0
    sum = 0
    
    for patterns, outputs in data:
        digits = dict()
        fives = list()
        sixs = list()
    
        # get basic numbers as 1, 4, 7, 8
        # and collect digits by segments
        for pattern in patterns:
            if len(pattern) == 2:
                digits["1"] = list(pattern)
            elif len(pattern) == 4:
                digits["4"] = list(pattern)
            elif len(pattern) == 3:
                digits["7"] = list(pattern)
            elif len(pattern) == 7:
                digits["8"] = list(pattern)
            elif len(pattern) == 5:
                fives.append(list(pattern))
            else:
                sixs.append(list(pattern))
    
        for one in digits["1"]:
            # get 6, rt
            for six in sixs[:]:
                if one not in six:
                    digits["6"] = six
                    rt = one
                    sixs.remove(six)
                    break
            if len(six) == 2:
                break
    
        # get rb
        for one in digits["1"]:
            if one is not rt:
                rb = one
                break
    
        # get 2, 3, 5
        for one in digits["1"]:
            for five in fives:
                if rt not in five:
                    digits["5"] = five
                elif rb not in five:
                    digits["2"] = five
                else:
                    digits["3"] = five
    
        lb = (set(digits["6"]) - set(digits["5"])).pop()
    
        # get 0, 9
        for six in sixs:
            if lb in six:
                digits["0"] = six
            else:
                digits["9"] = six
    
        digits = dict(zip(["".join(i) for i in digits.values()], digits.keys()))
        numbers = str()
    
        for output in outputs:
            if digits[output] in ["1", "4", "7", "8"]:
                count += 1
            numbers += digits[output]
        sum += int(numbers)
    
    print("Part One:", count)
    print("Part Two:", sum)
    
    3 votes
  11. bhrgunatha
    (edited )
    Link
    Part 1 Just count how many strings have the appropriate length in the output section (define (part-01 input) (define segments (list 2 3 4 7)) (for*/sum ([l input] [s (string-split (second...
    Part 1
    Just count how many strings have the appropriate length in the output section
    (define (part-01 input)
      (define segments (list 2 3 4 7))
      (for*/sum ([l input]
                 [s (string-split (second (string-split l " | ")))]
                 #:when (member (string-length s) segments))
        1))
    
    Part 2
    (define (part-02 input)
      (for/sum ([signal input])
        (unscramble signal)))
    

    I chose to represent a digit as a set of letters.
    I verified every line of input had 10 distinct sets because I was worried I'd need to think much harder about unscrambling the letter.
    I made a hash-table of segment count -> set of (set of letters) - since there are repeated digits.

    d1, d4, d7, and d8 all have unique segment counts (thank you Part 01!) so they were easy to find.

    Using set subtraction:

    • bd = d4 - d1

    Out of the 6-segment digits:

    • d9 is the only digit where d9 ⊇ d4 (actually d9⊃d4 but it wasn't needed)
    • d0 is the only digit where d0 ⊅ bd
    • d6 is the remaining digit

    5-segment digits:

    • d3 is the only digit where d3 ⊇ d1
    • d5 is the only digit where d5 ⊇ bd
    • d2 is the remaining digit

    Once I have the digits I made another hash-table to map dn -> number to calculate the output value.

    (define (unscramble signal)
      (match-define (list in out)
        (map digit-segments (string-split signal " | ")))
      (define in/out (for/set ([d (append in out)])
                       (letter-set d)))
    
      (define S (for/fold ([ls (hasheq)])
                          ([d in/out])
                  (hash-update ls (set-count d) (curryr set-add d) (seteq))))
    
      ;; 1, 4, 7, and 8 are singular
      (define d1 (set-first (hash-ref S 2)))
      (define d4 (set-first (hash-ref S 4)))
      (define d7 (set-first (hash-ref S 3)))
      (define d8 (set-first (hash-ref S 7)))
    
      (define bd (set-subtract d4 d1))
    
      ;; 6-segment digits: 0, 6, 9
      ;;     - only 9 contains 4
      ;;     - only 0 doesn't contain both b and d
      ;;     - otherwise it's 6
      (define-values (d0 d6 d9)
        (for/fold ([d0 #f] [d6 #f] [d9 #f])
                  ([s (hash-ref S 6)])
          (cond [(subset? d4 s) (values d0 d6 s)]
                [(not (subset? bd s)) (values s d6 d9)]
                [else (values d0 s d9)])))
    
      ;; 5-segment digits:
      ;;     - only 3 contains 1
      ;;     - only 5 contains b and d
      ;;     - otherwise it's 2
      (define-values (d2 d3 d5)
        (for/fold ([d2 #f] [d3 #f] [d5 #f])
                  ([s (hash-ref S 5)])
          (cond [(subset? d1 s) (values d2  s d5)]
                [(subset? bd s) (values d2 d3  s)]
                [else (values s d3 d5)])))
    
      (define digits (hash d0 0 d1 1 d2 2 d3 3 d4 4
                           d5 5 d6 6 d7 7 d8 8 d9 9))
      (decimal digits (map letter-set out)))
    
    (define (decimal digits letters)
      (for/fold ([sum 0])
                ([l letters])
        (+ (hash-ref digits l) (* 10 sum))))
    
    (define (digit-segments signal)
      (regexp-match* #rx"[a-g]+" signal))
    
    (define (letter-set s)
      (for/set ([c s]) c))
    

    I can't tell you how frustrating it is (and how much time I lost) assuming that decoding the scrambled segments was broken - and not the fact I just forgot to add d9 to the lookup table once I'd unscrambled it.

    Yesterday I gained 1974 places between solving part 1 and part 2 and today I lost 5052.

    3 votes
  12. [2]
    Comment deleted by author
    Link
    1. blitz
      Link Parent
      I spent so much time trying to figure out the math with set differences, it didn't even occur to me that I could also use set intersections!

      I spent so much time trying to figure out the math with set differences, it didn't even occur to me that I could also use set intersections!

      3 votes
  13. Kremor
    Link
    Solutions in Go: Part 1 func main() { var data [][]string = ReadData() count := 0 for _, row := range data { for _, digit := range row { switch len(digit) { case 2, 3, 4, 7: count += 1 } } }...

    Solutions in Go:

    Part 1
    func main() {
    	var data [][]string = ReadData()
    
    	count := 0
    	for _, row := range data {
    		for _, digit := range row {
    			switch len(digit) {
    			case 2, 3, 4, 7:
    				count += 1
    			}
    		}
    	}
    
    	fmt.Println(count)
    }
    

    This one was annoying, not because the problem was hard but because I had to write a lot of functions that would exist in other languages, like comparing the contents of two arrays. The snippet bellow only contains the code that deals with the problem, you can see the rest of the code here.

    Part 2
    func main() {
    	var data [][][][]int = ReadData()
    
    	res := 0
    	for _, row := range data {
    		res += Solve(row)
    	}
    
    	fmt.Println(res)
    }
    
    func Solve(data [][][]int) int {
    	one, input := FindByLength(data[0], 2)
    	seven, input := FindByLength(input, 3)
    	four, input := FindByLength(input, 4)
    	eight, input := FindByLength(input, 7)
    	three, input := FindByDiff(input, seven, 2, 5)
    	nine, input := FindByDiff(input, Combine(three, four), 0, 6)
    	five, input := FindByDiff(input, nine, 1, 5)
    	two, input := FindByLength(input, 5)
    	zero, input := FindByDiff(input, one, 4, 6)
    
    	solution := [][]int{
    		zero,
    		one,
    		two,
    		three,
    		four,
    		five,
    		input[0],
    		seven,
    		eight,
    		nine,
    	}
    
    	digits := []int{}
    
    	for _, n := range data[1] {
    		for i, _ := range solution {
    			if CompareSlice(n, solution[i]) {
    				digits = append(digits, i)
    			}
    		}
    	}
    
    	for i := 0; i < len(digits); i++ {
    		value += digits[i] * Raise(10, len(digits)-i)
    	}
    
    	value := 0
    
    	return value
    }
    
    func FindByDiff(input [][]int, r []int, d, l int) ([]int, [][]int) {
    	res := []int{}
    
    	for _, n := range input {
    		if len(n) != l {
    			continue
    		}
    		if Difference(n, r) == d {
    			res = n
    			break
    		}
    	}
    
    	return res, Remove(input, res)
    }
    
    func FindByLength(input [][]int, l int) ([]int, [][]int) {
    	res := []int{}
    
    	for _, n := range input {
    		if len(n) == l {
    			res = n
    			break
    		}
    	}
    
    	return res, Remove(input, res)
    }
    
    Theory for solution 2 Like everyone else I also used set theory, and since it seems there are multiple ways to solve it, here are the rules I used:
    • Every time a digit is identified remove it from the original set.
    • Find the digits 1, 4, 7, and 8.
    • |d3 ∆ d7| = |2|
    • |d9 ∆ (d3 ⋃ d4)| = |1|
    • |d9 ∆ d5| = |2|
    • At this point 2 is the only digit with five segments in the set
    • |d0 ∆ d1| = |4|
    • Finally 6 is the only digit left on the set

    Where:

    • ∆ - Objects that belong to A or B but not both.
    • |N| - Number of elements in the set.
    • ⋃ - Elements that belong to A or B.
    3 votes
  14. blitz
    (edited )
    Link
    I got completely nerdsniped by this and was up until 1:30 AM trying to solve it (new days drop at 10 PM for me). It wasn't working so I went to bed and in the morning I realized one of my...

    I got completely nerdsniped by this and was up until 1:30 AM trying to solve it (new days drop at 10 PM for me). It wasn't working so I went to bed and in the morning I realized one of my predicates was wrong!

    Rust on sourcehut

    It's real ugly, I like the way some other Rust users made theirs easier to read!

    3 votes
  15. errmeier
    Link
    Little bit late to this, but I'm proud of my solution for Part 2. The basic idea I went for was trying to identify the minimum amount of information needed to distinguish each of the digits. The...

    Little bit late to this, but I'm proud of my solution for Part 2. The basic idea I went for was trying to identify the minimum amount of information needed to distinguish each of the digits. The four factors I narrowed it down to were the length of the combination and the number of segments in common with the displays for 1, 4, and 7.
    Part 1: 80 bytes
    Part 2: 328 bytes

    Part 1
    print(sum(len(c)in(2,3,4,7) for I in open(0) for c in I.split('|')[1].split()))
    
    Part 2
    O={'6233':'0','2222':'1','5122':'2','5233':'3','4224':'4','5123':'5','6123':'6','3232':'7','7234':'8','6234':'9'}
    t = 0
    for I in open(0):l,r=I.strip().split('|');m={len(p):set(p) for p in l.split() if len(p) in (2,3,4)};t+=int(''.join(O[str(len(c))+''.join(str(len(set(c)&m[i])) for i in (2,3,4))] for c in r.split()))
    print(t)
    
    3 votes
  16. Crespyl
    Link
    Holy crap this one took me a while. "After some careful analysis" indeed. :P Thoughts/Spoilers So I was able to get straight to my core tactic of looking at intersections and differences between...

    Holy crap this one took me a while. "After some careful analysis" indeed. :P

    Thoughts/Spoilers

    So I was able to get straight to my core tactic of looking at intersections and differences between sets of lit segments for known/possible digits to build up a set of known segments.

    Unfortunately, trying to work out the exact process for narrowing things down from first principles was hard enough on its own, and then it took me a while to realize that the two given examples (the full set of lines, and the separate single line) were completely unrelated. So debugging my code on the first line of the sample input kept outputting what looked like garbage because the diagrams and discussion on the page no longer matched what I'd been looking at.

    Then, after sorting that out and thinking I had the right flow, I was able to solve the single line example and the first line of the bigger example, but the second was wrong and I had to track down some problem in my set logic for specific digits/segments.

    Going back and forth between the running logic in my debugger, the two different samples on the page, and trying to keep straight all the different segment/signal identifiers proved to be quite taxing (I'm going to put some of the blame on me being up late and starting to feel the effects of the booster shot I got earlier today; I swear I'm usually smarter than this :P)

    All together, this is some of the worst/ugliest code I've written in quite a while. I may or may not come back and clean it up later, or try implementing some other persons more elegant solution (I'm sure there's got to be at least a couple, and there might be some properties of the input that can be leaned on).

    Part 1 Ruby

    Part 1 is pretty easy, since we only need to care about the length of the items, not do any real figuring.

    def compute_p1(input)
      sum = 0
      input
        .lines
        .map(&:chomp)
        .map { _1.split(' | ').last }
        .map { _1.split(' ') }
        .map { _1.map(&:size) }
        .each do |outputs|
        sum += (outputs.count(2) + outputs.count(4) + outputs.count(3) + outputs.count(7))
      end
      return sum
    end
    
    Part 2 Ruby Since each line is entirely separate from the others, I've split out the core problem into its own function `decode` which just takes a single line and works out the output value. The real `compute_p2` function just maps that over the input lines and sums up the outputs.

    Oh boy here we go...

    #
    # 0:      1:      2:      3:      4:
    #  aaaa    ....    aaaa    aaaa    ....
    # b    c  .    c  .    c  .    c  b    c
    # b    c  .    c  .    c  .    c  b    c
    #  ....    ....    dddd    dddd    dddd
    # e    f  .    f  e    .  .    f  .    f
    # e    f  .    f  e    .  .    f  .    f
    #  gggg    ....    gggg    gggg    ....
    #
    #  5:      6:      7:      8:      9:
    #  aaaa    aaaa    aaaa    aaaa    aaaa
    # b    .  b    .  .    c  b    c  b    c
    # b    .  b    .  .    c  b    c  b    c
    #  dddd    dddd    ....    dddd    dddd
    # .    f  e    f  .    f  e    f  .    f
    # .    f  e    f  .    f  e    f  .    f
    #  gggg    gggg    ....    gggg    gggg
    #
    
    # digits with uniq segment active counts
    # 1 : 2
    # 4 : 4
    # 7 : 3
    # 8 : 7
    
    def decode(line)
      inputs, outputs = line.split(' | ').map {_1.split(' ')}
      # find the two signals for a '1'
      digit_1 = inputs.select{_1.size == 2}.first.chars.to_set
    
      # find the 4 signals for a '4'
      digit_4 = inputs.select{_1.size == 4}.first.chars.to_set
    
      # find the 3 signals for a '7'
      digit_7 = inputs.select{_1.size == 3}.first.chars.to_set
    
      # find the 7 signals for a '8'
      digit_8 = inputs.select{_1.size == 7}.first.chars.to_set
    
      # top segment 'a' is the difference between 1 and 7
      segment_a = digit_7 - digit_1
    
      # there are three digits with 6 lit segments, 0, 6, and 9
      # of those, all three have both segments from 1, except for 6 which only has the lower one
      digits_069 = inputs
                     .select{_1.size == 6}
                     .map {_1.chars.to_set}
    
      digit_6 = digits_069.reject { _1.superset?(digit_1) }.first
    
      segment_c = digit_1 - digit_6
      segment_f = digit_1 - segment_c
    
      segments_bd = (digit_6 & digit_4) - digit_1
      digit_0 = digits_069.reject {_1.superset?(segments_bd)}.first
      segment_d = segments_bd - digit_0
      segment_b = segments_bd - segment_d
    
      digits_235 = inputs
                     .select{_1.size == 5}
                     .map {_1.chars.to_set}
    
      segment_g = digits_235.map {_1 - segment_a - segment_b - segment_c - segment_d - segment_f}
                    .inject(:&)
    
      segment_e = digit_8 - segment_a - segment_b - segment_c - segment_d - segment_f - segment_g
    
      # ok we have all the segments mapped out
      # now make the remaining digits
    
      #digit_0
      #digit_1
      digit_2 = digit_8 - segment_b - segment_f
      digit_3 = digit_8 - segment_b - segment_e
      #digit_4
      digit_5 = digit_8 - segment_c - segment_e
      #digit_6
      #digit_7
      #digit_8
      digit_9 = digit_8 - segment_e
    
      digits = {
        digit_0 => '0',
        digit_1 => '1',
        digit_2 => '2',
        digit_3 => '3',
        digit_4 => '4',
        digit_5 => '5',
        digit_6 => '6',
        digit_7 => '7',
        digit_8 => '8',
        digit_9 => '9'
      }
    
      outputs.map{_1.chars.to_set}.map{digits[_1]}.join.to_i
    
    end
    
    def compute_p2(input)
      input
        .lines
        .map {decode(_1)}
        .sum
    end
    
    2 votes
  17. jzimbel
    (edited )
    Link
    Elixir This one was fun, once I finally understood what it was asking for. A few features of Elixir saved me some time—mainly the ability to use basically any value as a map key and have it...

    Elixir

    This one was fun, once I finally understood what it was asking for. A few features of Elixir saved me some time—mainly the ability to use basically any value as a map key and have it compare correctly in lookups, thanks to persistent data structures. This meant I could parse each group of characters as a set, and keep them as sets throughout the rest of the code rather than converting to strings or something for lookups. I also didn't have to deal with converting the list of digits to a single integer, thanks to the goofily named Integer.undigits/2

    Both parts
    defmodule AdventOfCode.Solution.Year2021.Day08 do
      def part1(input) do
        input
        |> parse_input()
        |> Enum.map(fn {_signal_char_sets, output_char_sets} ->
          Enum.count(output_char_sets, &is_1_4_7_or_8?/1)
        end)
        |> Enum.sum()
      end
    
      def part2(input) do
        input
        |> parse_input()
        |> Enum.map(&get_output_value/1)
        |> Enum.sum()
      end
    
      defp is_1_4_7_or_8?(char_set) do
        MapSet.size(char_set) in [2, 4, 3, 7]
      end
    
      defp get_output_value({signal_char_sets, output_char_sets}) do
        char_set_to_int = get_char_set_to_int(signal_char_sets)
    
        output_char_sets
        |> Enum.map(&char_set_to_int[&1])
        |> Integer.undigits()
      end
    
      defp get_char_set_to_int(signal_char_sets) do
        # Import relevant MapSet functions to save having to type out the module every time
        import MapSet, only: [difference: 2, equal?: 2, size: 1, subset?: 2, union: 2]
    
        # Similarly, create a closure to avoid having to explicitly pass signal_char_sets to Enum.find/2 every time
        find = &Enum.find(signal_char_sets, &1)
    
        one = find.(&(size(&1) == 2))
        four = find.(&(size(&1) == 4))
        seven = find.(&(size(&1) == 3))
        eight = find.(&(size(&1) == 7))
    
        segment_a = difference(seven, one)
        segments_b_d = difference(four, one)
        nine = find.(&(size(&1) == 6 and subset?(four, &1)))
        segment_g = difference(nine, union(four, segment_a))
        three = find.(&(size(&1) == 5 and subset?(one, &1)))
        segment_d = difference(three, union_all([one, segment_a, segment_g]))
        zero = find.(&(size(&1) == 6 and not subset?(segment_d, &1)))
        six = find.(&(size(&1) == 6 and not equal?(&1, nine) and not equal?(&1, zero)))
        segment_b = difference(segments_b_d, segment_d)
        two = find.(&(size(&1) == 5 and not equal?(&1, three) and not subset?(segment_b, &1)))
        five = find.(&(size(&1) == 5 and not equal?(&1, three) and subset?(segment_b, &1)))
    
        %{
          zero => 0,
          one => 1,
          two => 2,
          three => 3,
          four => 4,
          five => 5,
          six => 6,
          seven => 7,
          eight => 8,
          nine => 9
        }
      end
    
      defp parse_input(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_line/1)
      end
    
      defp parse_line(line) do
        ~r/([^|]+) \| ([^|]+)/
        |> Regex.run(line, capture: :all_but_first)
        |> Enum.map(&string_to_char_sets/1)
        |> List.to_tuple()
      end
    
      defp string_to_char_sets(string) do
        string
        |> String.split()
        |> Enum.map(&for <<char <- &1>>, into: MapSet.new(), do: char)
      end
    
      defp union_all(map_sets) do
        Enum.reduce(map_sets, &MapSet.union/2)
      end
    end
    

    Edit
    I really liked wycy's solution, so I stole it. 😏
    Substantially shorter code, and it runs faster too!

    Both parts, with a cleaner approach for part 2
    defmodule AdventOfCode.Solution.Year2021.Day08 do
      def part1(input) do
        input
        |> parse_input()
        |> Enum.map(fn {_signal_char_sets, output_char_sets} ->
          Enum.count(output_char_sets, &is_1_4_7_or_8?/1)
        end)
        |> Enum.sum()
      end
    
      def part2(input) do
        input
        |> parse_input()
        |> Enum.map(&get_output_value/1)
        |> Enum.sum()
      end
    
      defp is_1_4_7_or_8?(char_set) do
        MapSet.size(char_set) in [2, 4, 3, 7]
      end
    
      defp get_output_value({signal_char_sets, output_char_sets}) do
        char_set_to_int = get_char_set_to_int(signal_char_sets)
    
        output_char_sets
        |> Enum.map(&char_set_to_int[&1])
        |> Integer.undigits()
      end
    
      defp get_char_set_to_int(signal_char_sets) do
        one = Enum.find(signal_char_sets, &(MapSet.size(&1) == 2))
        four = Enum.find(signal_char_sets, &(MapSet.size(&1) == 4))
    
        decoded_signals = Enum.map(signal_char_sets, &decode_signal(&1, one, four))
    
        Enum.into(Enum.zip(signal_char_sets, decoded_signals), %{})
      end
    
      defp parse_input(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_line/1)
      end
    
      defp parse_line(line) do
        ~r/([^|]+) \| ([^|]+)/
        |> Regex.run(line, capture: :all_but_first)
        |> Enum.map(&string_to_char_sets/1)
        |> List.to_tuple()
      end
    
      defp string_to_char_sets(string) do
        string
        |> String.split()
        |> Enum.map(&for <<char <- &1>>, into: MapSet.new(), do: char)
      end
    
      defp decode_signal(signal, one, four) do
        import MapSet, only: [intersection: 2, size: 1]
    
        case {size(signal), size(intersection(signal, one)), size(intersection(signal, four))} do
          {6, 2, 3} -> 0
          {2, _, _} -> 1
          {5, 1, 2} -> 2
          {5, 2, 3} -> 3
          {4, _, _} -> 4
          {5, 1, 3} -> 5
          {6, 1, 3} -> 6
          {3, _, _} -> 7
          {7, _, _} -> 8
          {6, _, 4} -> 9
        end
      end
    end
    
    2 votes
  18. Gyrfalcon
    Link
    Real life is starting to catch up to me, but I'm going to keep chugging! This one was not too bad, just a small copy paste problem kept it from passing muster last night and getting posted. I am...

    Real life is starting to catch up to me, but I'm going to keep chugging! This one was not too bad, just a small copy paste problem kept it from passing muster last night and getting posted. I am proud of myself for reasoning out part 2, and I'm interested to see how others did it. My data structures overall and my efficiency with loops in part 2 may leave something to be desired, I was so focused on getting the logic correct that any concern for code quality went right out the window.

    Part 1
    import std/[strformat, sequtils, strutils, sugar]
    
    proc strToSet(input: string): set[char] = 
      var output: set[char]
    
      for character in input:
        incl(output, {character})
    
      return output
    
    proc parseFile(inputFile: string): seq[(seq[set[char]], seq[set[char]])] =
      var input: seq[string] = collect(newSeq):
        for line in inputFile.lines: line
    
      var output: seq[(seq[set[char]], seq[set[char]])]
    
      for line in input:
    
        var left, right: string
        (left, right) = split(line, " | ")
    
        output.add((mapIt(split(left, " "), strToSet(it)), 
                    mapIt(split(right, " "), strToSet(it))))
    
      return output
    
    func countUniques(data: seq[(seq[set[char]], seq[set[char]])]): int = 
      var totalUniques: int
      for line in data:
        for display in line[1]:
          if display.len == 2 or 
             display.len == 3 or 
             display.len == 4 or 
             display.len == 7:
            inc totalUniques
    
      return totalUniques
    
    
    proc main(inputFile: string) =
      
      var data = parseFile(inputFile)
    
      var numUniques = countUniques(data)
    
      echo &"The number of unique patterns displayed is {numUniques}"
      
    
    when is_main_module:
      # main("test.txt")
      main("input.txt")
    
    Part 2 Diff
    @@ -1,4 +1,4 @@
    -import std/[strformat, sequtils, strutils, sugar]
    +import std/[strformat, sequtils, strutils, sugar, tables, math]
     
     proc strToSet(input: string): set[char] = 
       var output: set[char]
    @@ -36,6 +36,78 @@ func countUniques(data: seq[(seq[set[char]], seq[set[char]])]): int =
     
       return totalUniques
     
    +proc solveLine(samples: seq[set[char]], display: seq[set[char]]): int =
    +  var mapping_from_display: Table[set[char], int]
    +  var mapping_to_display: Table[int, set[char]]
    +
    +  # mark the ones we know based on length
    +  for sample in samples:
    +    if sample.len == 2:
    +      mapping_from_display[sample] = 1
    +      mapping_to_display[1] = sample
    +    elif sample.len == 3:
    +      mapping_from_display[sample] = 7
    +      mapping_to_display[7] = sample
    +    elif sample.len == 4:
    +      mapping_from_display[sample] = 4
    +      mapping_to_display[4] = sample
    +    elif sample.len == 7:
    +      mapping_from_display[sample] = 8
    +      mapping_to_display[8] = sample
    +
    +  # 9 must be length of 6 and contain all segments of 4
    +  for sample in samples:
    +    if sample.len == 6 and
    +       mapping_to_display[4] < sample:
    +      mapping_to_display[9] = sample
    +      mapping_from_display[sample] = 9
    +      break
    +
    +  # 6 is length of 6 and but does not contain 1
    +  for sample in samples:
    +    if sample.len == 6 and
    +       not (mapping_to_display[1] < sample):
    +      mapping_to_display[6] = sample
    +      mapping_from_display[sample] = 6
    +      break
    +
    +  # 0 is of length 6 and is not 9 or 6
    +  for sample in samples:
    +    if sample.len == 6 and
    +       not (sample == mapping_to_display[9]) and
    +       not (sample == mapping_to_display[6]):
    +      mapping_to_display[0] = sample
    +      mapping_from_display[sample] = 0
    +      break
    +
    +  # 5 is the only one of length 5 to be a subset of 6
    +  for sample in samples:
    +    if sample.len == 5 and
    +       sample < mapping_to_display[6]:
    +      mapping_to_display[5] = sample
    +      mapping_from_display[sample] = 5
    +      break
    +
    +  # 2 is the only one left!
    +  for sample in samples:
    +    if sample.len == 5 and
    +        not (sample == mapping_to_display[3]) and
    +        not (sample == mapping_to_display[5]):
    +      mapping_to_display[2] = sample
    +      mapping_from_display[sample] = 2
    +  
    +  return 1000 * mapping_from_display[display[0]] +
    +          100 * mapping_from_display[display[1]] +
    +           10 * mapping_from_display[display[2]] +
    +            1 * mapping_from_display[display[3]]
     
     proc main(inputFile: string) =
       
    @@ -44,6 +116,10 @@ proc main(inputFile: string) =
       var numUniques = countUniques(data)
     
       echo &"The number of unique patterns displayed is {numUniques}"
    +
    +  var displaySum = sum(mapIt(data, solveLine(it[0], it[1])))
    +
    +  echo &"The sum total of the displays is {displaySum}"
    
    2 votes
  19. 3d12
    Link
    I'm so glad to see I'm not the only one who struggled through this one. I went through three different "mental models" of what the final code would look like, and at one point modeled out a...

    I'm so glad to see I'm not the only one who struggled through this one. I went through three different "mental models" of what the final code would look like, and at one point modeled out a SevenSegmentDisplay and SevenSegmentConsole object, and started implementing wire to display connections for each display to figure out what number it was... Before I realized that none of these numbers can figure it out themselves, they all need the information from each other to tell which number is which!

    My solution to part 2 is definitely not optimized, but I don't see anyone else using my same logic! I just implemented the exact steps I took as I was puzzling out the example on paper, with comments to keep my thoughts straight :)

    Part 1
    const fs = require('fs');
    const readline = require('readline');
    
    let inputArr = [];
    
    async function openFileForReading(file) {
    	const fileStream = fs.createReadStream(file);
    
    	const rl = readline.createInterface({
    		input: fileStream,
    		crlfDelay: Infinity
    	});
    
    	for await (const line of rl) {
    		try {
    			inputArr.push(line);
    		} catch(e) {
    			console.error(e);
    		}
    	}
    }
    
    function parseNumbers(line) {
    	let regex = /(\w+) (\w+) (\w+) (\w+) (\w+) (\w+) (\w+) (\w+) (\w+) (\w+) \| (\w+) (\w+) (\w+) (\w+)/;
    	let found = line.match(regex);
    	let signalPatterns = [];
    	let outputValues = [];
    	for (let i=1; i<11; i++) {
    		signalPatterns.push(found[i]);
    	}
    	for (let i=11; i<15; i++) {
    		outputValues.push(found[i]);
    	}
    	return { signalPatterns: signalPatterns, outputValues: outputValues };
    }
    
    (async function mainExecution() {
    	await openFileForReading('input.txt');
    	let totalAppearances = 0;
    	for (const line of inputArr) {
    		let parsed = parseNumbers(line);
    		totalAppearances += parsed.outputValues.filter(e =>
    				e.length === 2 || e.length === 4 || e.length === 3 || e.length === 7).length
    	}
    	console.log("Answer found! " + totalAppearances);
    })();
    
    Part 2
    const fs = require('fs');
    const readline = require('readline');
    
    let inputArr = [];
    
    async function openFileForReading(file) {
    	const fileStream = fs.createReadStream(file);
    
    	const rl = readline.createInterface({
    		input: fileStream,
    		crlfDelay: Infinity
    	});
    
    	for await (const line of rl) {
    		try {
    			inputArr.push(line);
    		} catch(e) {
    			console.error(e);
    		}
    	}
    }
    
    function parseNumbers(line) {
    	let regex = /(\w+) (\w+) (\w+) (\w+) (\w+) (\w+) (\w+) (\w+) (\w+) (\w+) \| (\w+) (\w+) (\w+) (\w+)/;
    	let found = line.match(regex);
    	let signalPatterns = [];
    	let outputValues = [];
    	for (let i=1; i<11; i++) {
    		signalPatterns.push(found[i]);
    	}
    	for (let i=11; i<15; i++) {
    		outputValues.push(found[i]);
    	}
    	return { signalPatterns: signalPatterns, outputValues: outputValues };
    }
    
    class SignalPatternConsole {
    	displays = [];
    
    	constructor(signalPatterns) {
    		// store signal patterns
    		for (const pattern of signalPatterns) {
    			this.displays.push({ wires: pattern, numericValue: undefined })
    		}
    
    		// compute
    		// step 1: identify 1, 4, 7, and 8
    		for (let display of this.displays) {
    			if (display.wires.length === 2) {
    				display.numericValue = 1;
    			}
    			if (display.wires.length === 3) {
    				display.numericValue = 7;
    			}
    			if (display.wires.length === 4) {
    				display.numericValue = 4;
    			}
    			if (display.wires.length === 7) {
    				display.numericValue = 8;
    			}
    		}
    
    		// step 2: identify the top segment
    		let one = this.displays.filter(e => e.numericValue === 1)[0];
    		let seven = this.displays.filter(e => e.numericValue === 7)[0];
    		let topSegment = 'x';
    		for (const segment of seven.wires) {
    			if (!one.wires.includes(segment)) {
    				topSegment = segment;
    			}
    		}
    
    		// step 3: determine which is zero based on the middle (found from
    		//		the difference between 4 and 1)
    		let four = this.displays.filter(e => e.numericValue === 4)[0];
    		let middleSegment = 'x';
    		let possibleMiddle = [];
    		for (const segment of four.wires) {
    			if (!one.wires.includes(segment)) {
    				possibleMiddle.push(segment);
    			}
    		}
    		let sixSegmentClub = this.displays.filter(e => e.wires.length === 6);
    		for (let member of sixSegmentClub) {
    			for (const possibility of possibleMiddle) {
    				if (!member.wires.includes(possibility)){
    					member.numericValue = 0;
    				}
    			}
    		}
    		let zero = this.displays.filter(e => e.numericValue === 0)[0];
    
    		// step 4: determining six based on missing one value from four
    		sixSegmentClub = this.displays.filter(e => e.numericValue === undefined && e.wires.length === 6);
    		for (let member of sixSegmentClub) {
    			let matches = 0;
    			for (const wire of four.wires) {
    				if (member.wires.includes(wire)) {
    					matches++;
    				}
    			}
    			if (matches === 3) {
    				member.numericValue = 6;
    			}
    		}
    		let six = this.displays.filter(e => e.numericValue === 6)[0];
    
    		// step 5: (freebie) nine is the last six-segment number
    		let nine = this.displays.filter(e => e.numericValue === undefined && e.wires.length === 6)[0];
    		nine.numericValue = 9;
    
    		// step 6: match 2 based on only having 2 segments from 4
    		let fiveSegmentClub = this.displays.filter(e => e.numericValue === undefined && e.wires.length === 5);
    		for (let member of fiveSegmentClub) {
    			let matches = 0;
    			for (const wire of four.wires) {
    				if (member.wires.includes(wire)) {
    					matches++;
    				}
    			}
    			if (matches === 2) {
    				member.numericValue = 2;
    			}
    		}
    		let two = this.displays.filter(e => e.numericValue === 2)[0];
    
    		// step 7: match 5 based on only having one segment from 1
    		fiveSegmentClub = this.displays.filter(e => e.numericValue === undefined && e.wires.length === 5);
    		for (let member of fiveSegmentClub) {
    			let matches = 0;
    			for (const wire of one.wires) {
    				if (member.wires.includes(wire)) {
    					matches++;
    				}
    			}
    			if (matches === 1) {
    				member.numericValue = 5;
    			}
    		}
    		let five = this.displays.filter(e => e.numericValue === 5)[0];
    
    		// step 8: last unassigned value is 3
    		let three = this.displays.filter(e => e.numericValue === undefined)[0];
    		three.numericValue = 3;
    
    	}
    
    	translationArray() {
    		let toReturn = [];
    		for (const display of this.displays) {
    			toReturn.push({ wires: display.wires, numericValue: display.numericValue });
    		}
    		return toReturn;
    	}
    }
    
    (async function mainExecution() {
    	await openFileForReading('input.txt');
    	let totalSum = 0;
    	for (const line of inputArr) {
    		let parsed = parseNumbers(line);
    		let cons = new SignalPatternConsole(parsed.signalPatterns);
    		let translation = cons.translationArray();
    		let values = parsed.outputValues;
    		let currentNumber = [];
    		for (const value of values) {
    			let searchList = translation.filter(e => (e.wires.length === value.length));
    			for (const wire of value) {
    				searchList = searchList.filter(e => (e.wires.includes(wire)));
    				if (searchList.length === 1) {
    					currentNumber.push(searchList[0].numericValue);
    					break;
    				}
    			}
    		}
    		console.log("DEBUG: Adding " + parseInt(currentNumber.join('')) + " to " + totalSum);
    		totalSum += parseInt(currentNumber.join(''));
    	}
    
    	console.log("Answer found! " + totalSum);
    })();
    
    2 votes