Skip Navigation

❄️ - 2023 DAY 3 SOLUTIONS -❄️

Day 3: Gear Ratios


Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • Code block support is not fully rolled out yet but likely will be in the middle of the event. Try to share solutions as both code blocks and using something such as https://topaz.github.io/paste/ or pastebin (code blocks to future proof it for when 0.19 comes out and since code blocks currently function in some apps and some instances as well if they are running a 0.19 beta)

FAQ


🔒This post will be unlocked when there is a decent amount of submissions on the leaderboard to avoid cheating for top spots

🔓 Edit: Post has been unlocked after 11 minutes

36 comments
  • Rust

    I've been using Regexes for every day so far, this time it helped in finding numbers along with their start and end position in a line. For the second part I mostly went with the approach of part 1 which was to look at all numbers and then figure out if it has a part symbol around it. Only in part 2 I saved all numbers next to a gear * in a hash table that maps each gear position to a list of adjacent numbers. Then in the end I can just look at all gears with exactly 2 numbers attached.

    Also it has to be said, multiplying two numbers is the exact opposite of getting their ratio!

  • Language: C#

    I aimed at keeping it as simple and short as reasonably possible this time, no overbuilding here!

    I even used a goto to let me break out of multiple loops at once 🤮 (I had to look up how they worked!) I would totally fail me in a code review!

  • Edit: Updated now with part 2.

    Managed to have a crack at this a bit earlier today, I've only done Part 01 so far. I'll update with Part 02 later.

    I tackled this with the personal challenge of not loading the entire puzzle input into memory, which would have made this a bit easier.

    Solution in Rust 🦀

    View formatted on GitLab

     rust
        
    use std::{
        env, fs,
        io::{self, BufRead, BufReader, Read},
    };
    
    fn main() -> io::Result<()> {
        let args: Vec = env::args().collect();
        let filename = &args[1];
        let file1 = fs::File::open(filename)?;
        let file2 = fs::File::open(filename)?;
        let reader1 = BufReader::new(file1);
        let reader2 = BufReader::new(file2);
    
        println!("Part one: {}", process_part_one(reader1));
        println!("Part two: {}", process_part_two(reader2));
        Ok(())
    }
    
    fn process_part_one(reader: BufReader) -> u32 {
        let mut lines = reader.lines().peekable();
        let mut prev_line: Option = None;
        let mut sum = 0;
        while let Some(line) = lines.next() {
            let current_line = line.expect("line exists");
            let next_line = match lines.peek() {
                Some(Ok(line)) => Some(line),
                Some(Err(_)) => None,
                None => None,
            };
            match (prev_line, next_line) {
                (None, Some(next)) => {
                    let lines = vec![¤t_line, next];
                    sum += parse_lines(lines, true);
                }
                (Some(prev), Some(next)) => {
                    let lines = vec![&prev, ¤t_line, next];
                    sum += parse_lines(lines, false);
                }
                (Some(prev), None) => {
                    let lines = vec![&prev, ¤t_line];
                    sum += parse_lines(lines, false);
                }
                (None, None) => {}
            }
    
            prev_line = Some(current_line);
        }
        sum
    }
    
    fn process_part_two(reader: BufReader) -> u32 {
        let mut lines = reader.lines().peekable();
        let mut prev_line: Option = None;
        let mut sum = 0;
        while let Some(line) = lines.next() {
            let current_line = line.expect("line exists");
            let next_line = match lines.peek() {
                Some(Ok(line)) => Some(line),
                Some(Err(_)) => None,
                None => None,
            };
            match (prev_line, next_line) {
                (None, Some(next)) => {
                    let lines = vec![¤t_line, next];
                    sum += parse_lines_for_gears(lines, true);
                }
                (Some(prev), Some(next)) => {
                    let lines = vec![&prev, ¤t_line, next];
                    sum += parse_lines_for_gears(lines, false);
                }
                (Some(prev), None) => {
                    let lines = vec![&prev, ¤t_line];
                    sum += parse_lines_for_gears(lines, false);
                }
                (None, None) => {}
            }
    
            prev_line = Some(current_line);
        }
    
        sum
    }
    
    fn parse_lines(lines: Vec<&String>, first_line: bool) -> u32 {
        let mut sum = 0;
        let mut num = 0;
        let mut valid = false;
        let mut char_vec: Vec> = Vec::new();
        for line in lines {
            char_vec.push(line.chars().collect());
        }
        let chars = match first_line {
            true => &char_vec[0],
            false => &char_vec[1],
        };
        for i in 0..chars.len() {
            if chars[i].is_digit(10) {
                // Add the digit to the number
                num = num * 10 + chars[i].to_digit(10).expect("is digit");
    
                // Check the surrounding character for non-period symbols
                for &x in &[-1, 0, 1] {
                    for chars in &char_vec {
                        if (i as isize + x).is_positive() && ((i as isize + x) as usize) < chars.len() {
                            let index = (i as isize + x) as usize;
                            if !chars[index].is_digit(10) && chars[index] != '.' {
                                valid = true;
                            }
                        }
                    }
                }
            } else {
                if valid {
                    sum += num;
                }
                valid = false;
                num = 0;
            }
        }
        if valid {
            sum += num;
        }
        sum
    }
    
    fn parse_lines_for_gears(lines: Vec<&String>, first_line: bool) -> u32 {
        let mut sum = 0;
        let mut char_vec: Vec> = Vec::new();
        for line in &lines {
            char_vec.push(line.chars().collect());
        }
        let chars = match first_line {
            true => &char_vec[0],
            false => &char_vec[1],
        };
        for i in 0..chars.len() {
            if chars[i] == '*' {
                let surrounding_nums = get_surrounding_numbers(&lines, i);
                let product = match surrounding_nums.len() {
                    0 | 1 => 0,
                    _ => surrounding_nums.iter().product(),
                };
                sum += product;
            }
        }
        sum
    }
    
    fn get_surrounding_numbers(lines: &Vec<&String>, gear_pos: usize) -> Vec {
        let mut nums: Vec = Vec::new();
        let mut num: u32 = 0;
        let mut valid = false;
        for line in lines {
            for (i, char) in line.chars().enumerate() {
                if char.is_digit(10) {
                    num = num * 10 + char.to_digit(10).expect("is digit");
                    if [gear_pos - 1, gear_pos, gear_pos + 1].contains(&i) {
                        valid = true;
                    }
                } else if num > 0 && valid {
                    nums.push(num);
                    num = 0;
                    valid = false;
                } else {
                    num = 0;
                    valid = false;
                }
            }
            if num > 0 && valid {
                nums.push(num);
            }
            num = 0;
            valid = false;
        }
        nums
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        const INPUT: &str = "467..114..
    ...*......
    ..35..633.
    ......#...
    617*......
    .....+.58.
    ..592.....
    ......755.
    ...$.*....
    .664.598..";
    
        #[test]
        fn test_process_part_one() {
            let input_bytes = INPUT.as_bytes();
            assert_eq!(4361, process_part_one(BufReader::new(input_bytes)));
        }
    
        #[test]
        fn test_process_part_two() {
            let input_bytes = INPUT.as_bytes();
            assert_eq!(467835, process_part_two(BufReader::new(input_bytes)));
        }
    }
    
      
  • [Rust] Harder one today, for part 1 I ended up getting stuck for a bit since I wasnt taking numbers at the end of lines into account and in part 2 I defined my gears vector in the wrong spot and spent a bit debugging that

    Code Link

  • I get the feeling that I should include some default types for handling 2D maps in my boilerplate, it's a very recurring problem in AoC after all.

    My solution is reasonably simplistic - and therefore also a bit slow, but the design meant I could do part 2 with just a few extra lines of code on the already processed data, here's the functional part of it; (I push the previous days solution as part of my workflow for starting with the current day so the full code won't be up until tomorrow) :::spoiler Ruby The code has been compressed for brevity.

     ruby
        
    Point = Struct.new('Point', :x, :y)
    PartNumber = Struct.new('PartNumber', :number, :adjacent) do
      def adjacent?(to); adjacent.include?(to); end
      def irrelevant?; adjacent.empty?; end
      def to_i; number; end
    end
    
    class Implementation
      def initialize
        @map = []; @dim = { width: 0, height: 0 }; @symbols = []; @numbers = []
      end
    
      def input(line)
        @dim[:width] = line.size; @dim[:height] += 1
        @map += line.chars
      end
    
      def calc
        for y in (0..@dim[:height]-1) do
          for x in (0..@dim[:width]-1) do
            chr = get(x, y); next if chr =~ /\d/ || chr == '.'
            @symbols << Point.new(x, y)
          end
        end
    
        for y in (0..@dim[:height]-1) do
          buf = ""; adj = []
          for x in (0..@dim[:width]) do # Going one over, to fake a non-number as an end char on all lines
            chr = get(x, y)
            if chr =~ /\d/
              buf += chr
              (-1..1).each do |adj_x|
                (-1..1).each do |adj_y|
                  next if adj_x == 0 && adj_y == 0 ||
                    (x + adj_x < 0) || (x + adj_x >= @dim[:width]) ||
                    (y + adj_y < 0) || (y + adj_y >= @dim[:height])
                  sym = Point.new(x + adj_x, y + adj_y)
                  adj << sym if @symbols.any? sym
                end
              end
            elsif !buf.empty?
              @numbers << PartNumber.new(buf.to_i, adj)
              buf = ""; adj = []
            end
          end
        end
      end
    
      def output
        part1 = @numbers.reject(&:irrelevant?).map(&:to_i).sum
        puts "Part 1:", part1
    
        gears = @symbols.select do |sym|
          next unless get(sym) == '*'
          next unless @numbers.select { |num| num.adjacent? sym }.size == 2
          true
        end
        part2 = gears.sum { |gear| @numbers.select { |num| num.adjacent? gear }.map(&:to_i).inject(:*) }
    
        puts "Part 2:", part2
      end
    
      private
    
      def get(x, y = -1)
        y = x.y if x.is_a?(Point)
        x = x.x if x.is_a?(Point)
        return unless (0..@dim[:width]-1).include?(x) && (0..@dim[:height]-1).include?(y)
    
        @map[y * @dim[:width] + x % @dim[:width]]
      end
    end
    
    
      

    :::

  • Crystal

    My computer crashed right most of the way through and I lost everything, so this was even more frustrating than it should have been
    Also damn, lemmy's tabs are massive

    will post part 2 when I get to it

     crystal
        
    input = File.read("input.txt")
    
    lines = input.lines.map(&.chars)
    
    sum = 0
    num_marker = nil
    lines.each_with_index do |line, y|
        line.each_with_index do |char, x|
            num_marker ||= x if char.number?
        
            if (!char.number? || x == line.size-1) && num_marker
                if check_symbol(y, (num_marker-1)..x, lines)
                    sum += lines[y][(char.number? ? num_marker..x : num_marker...x)].join.to_i 
                end
                num_marker = nil
    end end end
    puts sum
    
    def check_symbol(y, rx, lines)
        carr = [ lines[y][rx.begin]?, lines[y][rx.end]? ]
        carr += rx.map {|x| lines[y-1][x]? } if y > 0  
        carr += rx.map {|x| lines[y+1][x]? } if y < lines.size-1 
    
        carr.each {|c| return true if c && c != '.' && !c.number?}
        false
    end
    
      
  • Dart Solution

    Holy moley, if this year is intended to be impervious to AI solution, it's also working pretty well as a filter to this paltry Human Intelligence.

    Find interesting symbols, look around for digits, and expand these into numbers. A dirty hacky solution leaning on a re-used Grid class. Not recommended.

     undefined
        
    late ListGrid grid;
    
    Index look(Index ix, Index dir) {
      var here = ix;
      var next = here + dir;
      while (grid.isInBounds(next) && '1234567890'.contains(grid.at(next))) {
        here = next;
        next = here + dir;
      }
      return here;
    }
    
    /// Return start and end indices of a number at a point.
    (Index, Index) expandNumber(Index ix) =>
        (look(ix, Grid.dirs['L']!), look(ix, Grid.dirs['R']!));
    
    int parseNumber((Index, Index) e) => int.parse([
          for (var i = e.$1; i != e.$2 + Grid.dirs['R']!; i += Grid.dirs['R']!)
            grid.at(i)
        ].join());
    
    /// Return de-duplicated positions of all numbers near the given symbols.
    nearSymbols(Set syms) => [
          for (var ix in grid.indexes.where((i) => syms.contains(grid.at(i))))
            {
              for (var n in grid
                  .near8(ix)
                  .where((n) => ('1234567890'.contains(grid.at(n)))))
                expandNumber(n)
            }.toList()
        ];
    
    part1(List lines) {
      grid = ListGrid([for (var e in lines) e.split('')]);
      var syms = lines
          .join('')
          .split('')
          .toSet()
          .difference('0123456789.'.split('').toSet());
      // Find distinct number locations near symbols and sum them.
      return {
        for (var ns in nearSymbols(syms))
          for (var n in ns) n
      }.map(parseNumber).sum;
    }
    
    part2(List lines) {
      grid = ListGrid([for (var e in lines) e.split('')]);
      // Look for _pairs_ of numbers near '*' and multiply them.
      var products = [
        for (var ns in nearSymbols({'*'}).where((e) => e.length == 2))
          ns.map(parseNumber).reduce((s, t) => s * t)
      ];
      return products.sum;
    }
    
    
      
  • Language: Python

    Classic AoC grid problem... Tedious as usual, but very doable. Took my time and I'm pretty happy with the result. :]

    GitHub Repo

  • Another day of the 2023 Advent of Code, and another day where I hate looking at my code. This year just seems like it is starting off a lot more complex than I remember in previous years. This one was a little tricky, but I got there without any major setbacks. Another one I am excited to come back to and clean up, but this first pass is all about getting a solution, and this one works.

    https://github.com/capitalpb/advent_of_code_2023/blob/main/src/solvers/day03.rs

     rust
        
    #[derive(Clone, Copy, Debug)]
    struct Location {
        row: usize,
        start_col: usize,
        end_col: usize,
    }
    
    #[derive(Debug)]
    struct EngineSchematic {
        schematic: Vec>,
        numbers: Vec,
        symbols: Vec,
    }
    
    impl EngineSchematic {
        fn from(input: &str) -> EngineSchematic {
            let schematic: Vec> = input.lines().map(|line| line.chars().collect()).collect();
            let mut numbers = vec![];
            let mut symbols = vec![];
            let mut location: Option = None;
    
            for (row_index, row) in schematic.iter().enumerate() {
                for (col, ch) in row.iter().enumerate() {
                    match ch {
                        ch if ch.is_ascii_punctuation() => {
                            if let Some(location) = location {
                                numbers.push(location);
                            }
                            location = None;
    
                            if ch != &'.' {
                                symbols.push(Location {
                                    row: row_index,
                                    start_col: col,
                                    end_col: col,
                                });
                            }
                        }
                        ch if ch.is_digit(10) => {
                            if let Some(mut_location) = location.as_mut() {
                                mut_location.end_col = col;
                            } else {
                                location = Some(Location {
                                    row: row_index,
                                    start_col: col,
                                    end_col: col,
                                });
                            }
                        }
                        _ => {
                            unreachable!("malformed input");
                        }
                    }
                }
    
                if let Some(location) = location {
                    numbers.push(location);
                }
                location = None;
            }
    
            EngineSchematic {
                schematic,
                numbers,
                symbols,
            }
        }
    
        fn get_number_value(&self, location: &Location) -> u32 {
            self.schematic[location.row][location.start_col..=location.end_col]
                .iter()
                .collect::()
                .parse::()
                .unwrap()
        }
    
        fn is_gear(&self, location: &Location) -> bool {
            self.schematic[location.row][location.start_col] == '*'
        }
    
        fn are_adjacent(&self, location: &Location, other_location: &Location) -> bool {
            location.start_col >= other_location.start_col.checked_sub(1).unwrap_or(0)
                && location.end_col <= other_location.end_col + 1
                && (location.row == other_location.row
                    || location.row == other_location.row.checked_sub(1).unwrap_or(0)
                    || location.row == other_location.row + 1)
        }
    }
    
    pub struct Day03;
    
    impl Solver for Day03 {
        fn star_one(&self, input: &str) -> String {
            let schematic = EngineSchematic::from(input);
    
            schematic
                .numbers
                .iter()
                .filter(|number| {
                    schematic
                        .symbols
                        .iter()
                        .any(|symbol| schematic.are_adjacent(symbol, number))
                })
                .map(|number| schematic.get_number_value(number))
                .sum::()
                .to_string()
        }
    
        fn star_two(&self, input: &str) -> String {
            let schematic = EngineSchematic::from(input);
    
            schematic
                .symbols
                .iter()
                .filter(|symbol| schematic.is_gear(symbol))
                .map(|symbol| {
                    let adjacent_numbers = schematic
                        .numbers
                        .iter()
                        .filter(|number| schematic.are_adjacent(symbol, number))
                        .collect::>();
                    if adjacent_numbers.len() == 2 {
                        schematic.get_number_value(adjacent_numbers[0])
                            * schematic.get_number_value(adjacent_numbers[1])
                    } else {
                        0
                    }
                })
                .sum::()
                .to_string()
        }
    }
    
      
  • [LANGUAGE: C#]

    I kept trying to create clever solutions, but ended up falling back on regex when it was taking to long. THE TLDR is we scan the list of strings for a symbol, then parse the three lines above, below and inline with the symbol for digits. Then we try and match the indexes of the match and the area around the symbol. Part 2 was a small modification, and was mostly about getting the existing code to conform the data into a pattern for each of the three lines.

    Part 1

     undefined
            static char[] Symbols = { '@', '#', '$', '%', '&', '*', '/', '+', '-', '=' };
        string pattern = @"\d+";
        static List? list;
        list = new List((await File.ReadAllLinesAsync(@".\Day 3\PuzzleInput.txt")));
        
        int count = 0;
        for (int row = 0; row < list.Count; row++)
        {
            for (int col = 0; col < list[row].Length; col++)
            {
                var c = list[row][col];
                if (c == '.')
                {
                    continue;
                }
        
                if (Symbols.Contains(c))
                {
                    var res = Calculate(list[row - 1], col);
                    res += Calculate(list[row], col);
                    res += Calculate(list[row + 1], col);
                    count += res;
                }
        
            }
        }
        Console.WriteLine(count);
        
        private static int Calculate(string line, int col)
        {
            List indexesToCheck = new List { col - 1, col, col + 1 };
            int count = 0;
            MatchCollection matches = Regex.Matches(line, pattern);
        
            foreach (Match match in matches)
            {
                string number = match.Value;
        
                if (AnyIndexInList(indexesToCheck, match.Index, match.Length))
                {
                    count += Int32.Parse(number);
                }
            }
            return count;
        }
        
        static bool AnyIndexInList(List list, int startIndex, int length)
        {
            for (int i = startIndex; i < startIndex + length; i++)
            {
                if (list.Contains(i))
                {
                    return true;
                }
            }
            return false;
        }
    
    
      

    Part 2:

     undefined
            list = new List((await File.ReadAllLinesAsync(@".\Day 3\PuzzleInput.txt")));
        
        int count = 0;
        for (int row = 0; row < list.Count; row++)
        {
            for (int col = 0; col < list[row].Length; col++)
            {
                var c = list[row][col];
                if (c == '.')
                    continue;
                
                if (c == '*')
                {
                    var res1 = Calculate2(list[row - 1], col);
                    var res2 = Calculate2(list[row], col);
                    var res3 = Calculate2(list[row + 1], col);
        
                    count += (res1, res2, res3) switch 
                    {
                        {res1: not null, res2: null, res3: null } when  res1[1] != null => res1[0].Value * res1[1].Value,
                        {res1:  null, res2: not null, res3: null } when res2[1] != null => res2[0].Value * res2[1].Value,
                        {res1:  null, res2: null, res3: not null } when res3[1] != null => res3[0].Value * res3[1].Value,
        
                        {res1: not null, res2: not null, res3: null } => res1[0].Value * res2[0].Value,
                        {res1: not null, res2: null, res3: not null } => res1[0].Value * res3[0].Value,
                        {res1: null, res2: not null, res3: not null } => res2[0].Value * res3[0].Value,
                        {res1: not null, res2: not null, res3: not null } => res1[0].Value * res2[0].Value * res3[0].Value,
        
                        _ => 0
                    } ;
                }
            }
        }
                        
        Console.WriteLine(count);
    
    
        private static int?[]? Calculate2(string line, int col)
        {
            List indexesToCheck = new List { col - 1, col, col + 1 };
            int?[]? count = null;
            MatchCollection matches = Regex.Matches(line, pattern);
        
            foreach (Match match in matches)
            {
                string number = match.Value;
        
                if (AnyIndexInList(indexesToCheck, match.Index, match.Length))
                {
                    if (count == null)
                        count = new int?[2] { Int32.Parse(number), null };
                    else {
                        count[1] = Int32.Parse(number);
                    };
                }
            }
            return count;
        }
      
  • Factor on github (with comments and imports):

     undefined
        
    : symbol-indices ( line -- seq )
      [ ".0123456789" member? not ] find-all [ first ] map
    ;
    
    : num-spans ( line -- seq )
      >array [ over digit? [ nip ] [ 2drop f ] if ] map-index
      { f } split harvest
      [ [ first ] [ last ] bi 2array ] map
    ;
    
    : adjacent? ( num-span symbol-indices -- ? )
      swap [ first 1 - ] [ last 1 + ] bi [a,b]
      '[ _ interval-contains? ] any?
    ;
    
    : part-numbers ( line nearby-symbol-indices -- seq )
      [ dup num-spans ] dip
      '[ _ adjacent? ] filter
      swap '[ first2 1 + _ subseq string>number ] map
    ;
    
    : part1 ( -- )
      "vocab:aoc-2023/day03/input.txt" utf8 file-lines
      [ [ symbol-indices ] map ] keep
      [
        pick swap [ 1 - ?nth-of ] [ nth-of ] [ 1 + ?nth-of ] 2tri
        3append part-numbers sum
      ] map-index sum nip .
    ;
    
    : star-indices ( line -- seq )
      [ CHAR: * = ] find-all [ first ] map
    ;
    
    : gears ( line prev-line next-line -- seq-of-pairs )
      pick star-indices
      [ 1array '[ _ part-numbers ] [ 3dup ] dip tri@ 3append ]
      [ length 2 = ] map-filter [ 3drop ] dip
    ;
    
    : part2 ( -- )
      "vocab:aoc-2023/day03/input.txt" utf8 file-lines
      dup [
        pick swap [ 1 - ?nth-of ] [ 1 + ?nth-of ] 2bi
        gears [ product ] map-sum
      ] map-index sum nip .
    ;
    
      
  • [Language: Lean4]

    I'll only post the actual parsing and solution. I have written some helpers which are in other files, as is the main function. For the full code, please see my github repo.

    Here I used HashMap and HashSet, but that's just an optimization. I'm not even sure if they are faster than just using lists here...

36 comments