12 votes

Day 4: Scratchcards

Today's problem description: https://adventofcode.com/2023/day/4

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>

33 comments

  1. [2]
    whs
    (edited )
    Link
    Language of the Day for Day 4 is C! I think I'll try to use the harder languages first before going to simpler languages. After 2 days of functional programming I think I have had enough. #include...

    Language of the Day for Day 4 is C! I think I'll try to use the harder languages first before going to simpler languages. After 2 days of functional programming I think I have had enough.

    #include <stdio.h>
    #include <stdint.h>
    #include <stdlib.h>
    
    typedef struct {
        uint8_t no;
        // the list is zero terminated, this assume input has no zero
        uint8_t winning[10];
        uint8_t have[25];
    } card_t;
    
    // Read a card_t. The caller is responsible for freeing card_t
    // May return NULL if read fail
    card_t* read_card() {
        int err;
        // calloc zero fill the output
        card_t* out = calloc(sizeof(card_t), 1);
        err = scanf("Card %u: ", &out->no);
        if (err != 1) {
            return NULL;
        }
        for (int i = 0; i < sizeof(out->winning); i++) {
            err = scanf("%d ", &out->winning[i]);
            if (err != 1) {
                break;
            }
        }
        scanf(" | ");
        for (int i = 0; i < sizeof(out->have); i++) {
            err = scanf("%d ", &out->have[i]);
            if (err != 1) {
                break;
            }
        }
    
        return out;
    }
    
    // Return number of winnings
    int winnings(card_t* card) {
        int combo = 0;
        for (int i = 0; i < sizeof(card->have); i++) {
            int have = card->have[i];
            if (have == 0) {
                break;
            }
            for (int j = 0; j < sizeof(card->winning); j++) {
                int winning = card->winning[j];
                if (winning == 0) {
                    break;
                }
                if (have == card->winning[j]) {
                    combo++;
                    break;
                }
            }
        }
        return combo;
    }
    
    int solve(card_t* card) {
        int out = winnings(card);
        if (out == 0) {
            return 0;
        }
        // 2^(n-1)
        return 1 << (out-1);
    }
    
    int main() {
        /*/
        // Part 1
        int worth_sum = 0;
        for (;;) {
            card_t* card = read_card();
            if (card == NULL) {
                break;
            }
            int worth = solve(card);
            worth_sum += worth;
            free(card);
        }
        printf("%d", worth_sum);
        /**/
    
        /**/
        // Part 2
    #define MAX_GAMES 193
        card_t *games[MAX_GAMES] = {NULL};
        int cardCopies[MAX_GAMES] = {0};
        for(int i = 0; i < MAX_GAMES; i++) {
            games[i] = read_card();
            if(games[i] == NULL) {
                break;
            }
            cardCopies[i] = 1;
        }
        for(int i = 0; i < MAX_GAMES; i++) {
            if(games[i] == NULL) {
                break;
            }
            int winning = winnings(games[i]);
            while(winning > 0) {
                cardCopies[i + winning] += cardCopies[i];
                winning--;
            }
        }
        int totalCard = 0;
        for(int i = 0; i < MAX_GAMES; i++) {
            totalCard += cardCopies[i];
        }
        printf("%d\n", totalCard);
        /**/
    
        return 0;
    }
    

    I thought parsing would be complicated, but turns out scanf works pretty well since the problem has fixed input size (although different between example and real inputs). Initially the winnings and solve function was the same function, but I had to refactor that to reuse it for part 2.

    Two tricks in my code:

    • /*/ ... /**/ is a good way to make commented out block of code. Just add another * to make the first block /**/ to uncomment the entire block.
    • 1<<n is the same as 2^n
    4 votes
    1. tjf
      Link Parent
      Cool comment trick I hadn't seen before. I'm fond of using #if 0 / #endif for this and toggling between 0 and 1.

      Cool comment trick I hadn't seen before. I'm fond of using #if 0 / #endif for this and toggling between 0 and 1.

      2 votes
  2. [3]
    lily
    Link
    I did a lot better than I expected on this one, getting top 500 on the leaderboard. Funnily enough, I originally solved part 2 with the naïve solution that processed each copy again as if it were...

    I did a lot better than I expected on this one, getting top 500 on the leaderboard. Funnily enough, I originally solved part 2 with the naïve solution that processed each copy again as if it were the original card, and especially using PyPy, the computation time really wasn't a big deal (only about 5 seconds or so). If performance was supposed to be a bottleneck for today's problem, the input probably should've been longer. I wasn't happy with that solution, though, and reworked my program to actually be efficient. I expect this is more what was intended.

    Solution
    # Advent of Code 2023
    # Day 4: Scratchcards
    
    point_sum = 0
    copy_amounts = {}
    
    with open("inputs/day_4.txt") as file:
        for i, line in enumerate(file):
            halves = line.split(": ")[1].split("|")
            winning_nums = [int(num) for num in halves[0].split()]
    
            points = 0
            winning_amount = 0
    
            for num in [int(num) for num in halves[1].split()]:
                if num in winning_nums:
                    if points == 0:
                        points = 1
                    else:
                        points *= 2
    
                    winning_amount += 1
    
            point_sum += points
            copy_amounts[i] = winning_amount
    
    print(f"Part 1: {point_sum}")
    
    amounts = {card: 1 for card in copy_amounts}
    
    for card in amounts:
        for i in range(copy_amounts[card]):
            amounts[card + i + 1] += amounts[card]
    
    print(f"Part 2: {sum(amounts.values())}")
    
    3 votes
    1. [2]
      tjf
      Link Parent
      A fellow PyPy user! I always throw it in my shebang for AoC for the free speed boost.

      A fellow PyPy user! I always throw it in my shebang for AoC for the free speed boost.

      3 votes
      1. lily
        Link Parent
        It's always useful to try to force a brute force solution to work while I try to write something better, haha.

        It's always useful to try to force a brute force solution to work while I try to write something better, haha.

        3 votes
  3. [3]
    tjf
    Link
    Not too bad today, and I always love an opportunity to use sets (even if they weren't strictly necessary). My Python solutions: Part 1 #!/usr/bin/env pypy3 total = 0 for line in open(0): winning,...

    Not too bad today, and I always love an opportunity to use sets (even if they weren't strictly necessary). My Python solutions:

    Part 1
    #!/usr/bin/env pypy3
    
    total = 0
    for line in open(0):
        winning, mine = (set(map(int, part)) for part in
                         map(str.split, line.strip().split(':')[1].split('|')))
        nmatches = len(winning & mine)
        total += int(2 ** (nmatches - 1))
    
    print(total)
    

    Had to re-read part two's instructions to parse out exactly how copies worked, but straightforward enough once I knew what was being asked.

    Part 2
    #!/usr/bin/env pypy3
    
    from collections import defaultdict
    
    copies = defaultdict(lambda: 1)
    for i, line in enumerate(open(0), start=1):
        copies[i]   # defaultdict hack
        winning, mine = (set(map(int, part)) for part in
                         map(str.split, line.strip().split(':')[1].split('|')))
        nmatches = len(winning & mine)
        for j in range(i + 1, i + nmatches + 1):
            copies[j] += copies[i]
    
    print(sum(copies.values()))
    
    2 votes
    1. [2]
      Eabryt
      (edited )
      Link Parent
      Quick question. I'm doing something pretty similar to you (grabbing the winning/mine and overlaps slightly different, but results in the same), but when I submit my answer for part 2 it's telling...

      Quick question.

      I'm doing something pretty similar to you (grabbing the winning/mine and overlaps slightly different, but results in the same), but when I submit my answer for part 2 it's telling me my answer is too high. Would you maybe be able to point me in the correct direction?

      Edit: Never mind, it was my damn REGEX, as always.

      Part 2
      def part2(lines):
          print(f"Part 2!")
          cards = defaultdict(lambda: 1)
          for line in lines:
              card = int(re.search(r'Card.*(\d+):', line).group(1))  # <- This needs to capture on \s+(\d+), otherwise it doesn't grab all digits, just the last place.
              cards[card]
              winning, mine = map(lambda x: list(map(int, x.split())), line.strip().split(":")[1].split("|"))
              overlap = len(set(winning) & set(mine))
              for x in range(card + 1, card + overlap + 1):
                  cards[x] += cards[card]
          print(f"Result: {sum(cards.values())}")
      
      2 votes
      1. tjf
        Link Parent
        Nice! The most annoying bugs are always in the parsing...

        Nice! The most annoying bugs are always in the parsing...

        1 vote
  4. tomf
    Link
    I haven't even read part two... but here's part one in Google Sheets. Its similar to another solution I saw in a chat. I don't see any other way around it. Part 1 =ARRAYFORMULA( SUM( MAP( A:A,...

    I haven't even read part two... but here's part one in Google Sheets. Its similar to another solution I saw in a chat. I don't see any other way around it.

    Part 1
    =ARRAYFORMULA(
      SUM(
       MAP(
        A:A,
        LAMBDA(
         x,
         LET(
          d,SPLIT(
             TOCOL(
              SPLIT(
               REGEXEXTRACT(x,": (.*)"),
               "|"),
              3),
             " "),
          t,SUM(COUNTIF(INDEX(d,2),INDEX(d,1)))-1,
          IF(t=-1,0,2^t))))))
    
    2 votes
  5. infinitesimal
    Link
    I can't find Kotlin's integer exponent operator or function. Part 2 also took a whole 1.5 seconds to do naively before I read the first comment by @lily and only tracked the counts after which it...

    I can't find Kotlin's integer exponent operator or function. Part 2 also took a whole 1.5 seconds to do naively before I read the first comment by @lily and only tracked the counts after which it went back to instantaneous.

    Kotlin
    import kotlin.math.pow
    
    object Day4 {
        data class Card(val id: Int, val winners: Set<Int>, val havers: Set<Int>)
    
        private fun parse(line: String) = let {
            fun numsOf(s: String) = Regex("""\d+""").findAll(s).map { it.value.toInt() }.toSet()
            fun idOf(s: String) = Regex("""\d+""").find(s)!!.value.toInt()
            line.split(":").let {
                val (winners0, havers0) = it[1].split("|")
                Card(idOf(it[0]), numsOf(winners0), numsOf(havers0))
            }
        }
    
        fun part1(lines: List<String>) = lines.sumOf { line ->
            val (_, winners, havers) = parse(line)
            val inter = winners.intersect(havers)
            if (inter.isNotEmpty()) 2.0.pow(inter.size - 1).toInt() else 0
        }
    
        fun part2(lines: List<String>) = let {
            val cards = lines.map { parse(it) }
            val counts = Array<Int>(cards.size + 1) { if (it == 0) 0 else 1 }
            cards.forEach { (id, winners, havers) ->
                ((id + 1)..(id + winners.intersect(havers).size)).forEach { counts[it] += counts[id] }
            }
            counts.sum()
        }
    
        val input = javaClass.getResourceAsStream("${javaClass.name}.txt")!!.reader().readLines()
    }
    
    fun main() {
        with(Day4) {
            println(part1(input))
            println(part2(input))
        }
    }
    
    2 votes
  6. [10]
    DataWraith
    Link
    I was a bit late to the party, but now I'm all caught up. I struggled a bit with Day 1 and Day 3, where I passed the test case initially, but not the actual puzzle input. Day 4 was straightforward...

    I was a bit late to the party, but now I'm all caught up. I struggled a bit with Day 1 and Day 3, where I passed the test case initially, but not the actual puzzle input. Day 4 was straightforward in comparison.

    Rust
    fn main() {
        println!("Part 1: {}", part1(include_str!("../input.txt")));
        println!("Part 2: {}", part2(include_str!("../input.txt")));
    }
    
    #[derive(Debug)]
    struct Card {
        id: usize,
        winning_numbers: Vec<usize>,
        numbers_you_have: Vec<usize>,
    }
    
    impl Card {
        fn num_matches(&self) -> usize {
            let mut count = 0;
    
            for number in self.numbers_you_have.iter() {
                if self.winning_numbers.contains(number) {
                    count += 1;
                }
            }
    
            count
        }
    
        fn score(&self) -> usize {
            let count = self.num_matches();
    
            if count == 0 {
                return 0;
            }
    
            2usize.pow((count - 1) as u32)
        }
    }
    
    impl From<&str> for Card {
        fn from(value: &str) -> Self {
            let split = value.split(": ").collect::<Vec<_>>();
            let id = split[0]
                .split(' ')
                .last()
                .unwrap()
                .parse::<usize>()
                .unwrap();
            let numbers = split[1].split(" | ").collect::<Vec<_>>();
    
            let winning_numbers = numbers[0]
                .trim()
                .split(' ')
                .filter(|n| !n.is_empty())
                .map(|n| n.parse::<usize>().unwrap())
                .collect::<Vec<_>>();
    
            let numbers_you_have = numbers[1]
                .trim()
                .split(' ')
                .filter(|n| !n.is_empty())
                .map(|n| n.parse::<usize>().unwrap())
                .collect::<Vec<_>>();
    
            Card {
                id,
                winning_numbers,
                numbers_you_have,
            }
        }
    }
    
    fn parse_cards(input: &str) -> Vec<Card> {
        input.lines().map(Card::from).collect()
    }
    
    fn part1(input: &str) -> usize {
        parse_cards(input).iter().map(|card| card.score()).sum()
    }
    
    fn part2(input: &str) -> usize {
        let cards = parse_cards(input);
        let mut count = 0;
    
        let num_matches = cards
            .iter()
            .map(|card| card.num_matches())
            .collect::<Vec<_>>();
    
        let mut num_copies = vec![1; cards.len()];
    
        for i in 0..cards.len() {
            count += num_copies[i];
    
            if num_matches[i] == 0 {
                continue;
            }
    
            for j in (i + 1)..=(i + num_matches[i]) {
                num_copies[j] += num_copies[i];
            }
        }
    
        count
    }
    

    I did this in the most naive way I could think of, so it's kind of slow at 35ms for both parts. A set data structure would probably be much faster for finding the winning numbers, but it's fast enough, so I won't bother optimizing it right now.

    2 votes
    1. [9]
      Toric
      Link Parent
      Looks like the main difference between our solutions is the use of that set. I was able to get mine down to 24ms on the debug build, ~5ms with optimizations turned on.

      Looks like the main difference between our solutions is the use of that set. I was able to get mine down to 24ms on the debug build, ~5ms with optimizations turned on.

      1 vote
      1. [6]
        jzimbel
        Link Parent
        Out of curiosity, how are you timing your program? Is it 5ms to solve both parts? Asking because the benchmarks for my solution in elixir are consistently around 4.5 ms or less for each part—I...

        Out of curiosity, how are you timing your program? Is it 5ms to solve both parts? Asking because the benchmarks for my solution in elixir are consistently around 4.5 ms or less for each part—I really would not have expected it to run faster than a rust solution!

        Specifically, I'm timing only the evaluation of each solution function, which receives the puzzle input already loaded from file, as a string argument. Running on a 2019 MBP.

        $ mix do advent.solve -d4 -p1 -b + advent.solve -d4 -p2 -b
        # (Benchmark for part 1 solution fn)
        Benchmarking result ...
        
        Name             ips        average  deviation         median         99th %
        result        233.56        4.28 ms     ±6.42%        4.23 ms        5.42 ms
        
        # (Benchmark for part 2 solution fn)
        Benchmarking result ...
        
        Name             ips        average  deviation         median         99th %
        result        224.13        4.46 ms     ±9.05%        4.35 ms        6.04 ms
        
        1 vote
        1. [4]
          whispersilk
          Link Parent
          I haven't looked at Toric's code yet, so I don't know what we're doing differently in terms of parsing, but I'm using Rust too and with optimizations turned on, both parts together take 345 μs...

          I haven't looked at Toric's code yet, so I don't know what we're doing differently in terms of parsing, but I'm using Rust too and with optimizations turned on, both parts together take 345 μs including the time it takes to read the file from disk for each part. Without optimizations it's more like 3.1 ms.

          2 votes
          1. jzimbel
            Link Parent
            Ah ok, that's more what I'd expect. I was about to be pretty shocked by how much optimization the erlang VM does.

            Ah ok, that's more what I'd expect. I was about to be pretty shocked by how much optimization the erlang VM does.

            1 vote
          2. [2]
            Toric
            Link Parent
            How are you parsing? Im doing parsing via nom, which is probably not the most efficient way out there.

            How are you parsing? Im doing parsing via nom, which is probably not the most efficient way out there.

            1 vote
            1. whispersilk
              Link Parent
              I'm sticking to the standard library only for as long as that's tenable, so I'm just splitting on spaces etc and using str::parse<u32> to get numbers out. My code is linked down below. Having...

              I'm sticking to the standard library only for as long as that's tenable, so I'm just splitting on spaces etc and using str::parse<u32> to get numbers out. My code is linked down below.

              Having taken a look at your code now, I think the speed difference is probably coming from the fact that you're storing winning_numbers and numbers in BTreeSets and performing intersections as you go. I just calculated how many numbers on each card were winners at the time of parsing and stored that, so I never paid for node allocation or the cache misses involved in searching.

              2 votes
        2. Toric
          Link Parent
          just the unix time command, nothing fancy. it includes the startup time of the program, but not any reading a file from disk, as the file is actually pre-compiled into a string in the binary.

          just the unix time command, nothing fancy. it includes the startup time of the program, but not any reading a file from disk, as the file is actually pre-compiled into a string in the binary.

          1 vote
      2. [2]
        DataWraith
        Link Parent
        Hm. I just ran the benchmark again to check, and it seems like I screwed up the hyperfine invocation somehow the first time around. That's what you get when you're too lazy to do an actual...

        Hm. I just ran the benchmark again to check, and it seems like I screwed up the hyperfine invocation somehow the first time around. That's what you get when you're too lazy to do an actual benchmark with criterion or divan...

        Benchmark 1: target/release/day-04
          Time (mean ± σ):       1.7 ms ±   0.4 ms    [User: 1.3 ms, System: 0.3 ms]
          Range (min … max):     1.0 ms …   2.5 ms    1749 runs
        
         Benchmark 1: target/debug/day-04
          Time (mean ± σ):       6.2 ms ±   1.7 ms    [User: 5.6 ms, System: 0.5 ms]
          Range (min … max):     3.7 ms …   8.8 ms    382 runs
        

        This does include the startup time of the executable; I may do a proper benchmark after I solve today's problem.

        1 vote
        1. Toric
          (edited )
          Link Parent
          heh, I was just using the unix time command, ill have to rerun with hyperfine. EDIT: hyperfine -N -w5 says ~3.5 ms

          heh, I was just using the unix time command, ill have to rerun with hyperfine.

          EDIT: hyperfine -N -w5 says ~3.5 ms

          1 vote
  7. wycy
    Link
    Rust Day 4 use std::env; use std::io::{self, prelude::*, BufReader}; use std::fs::File; use std::collections::{HashSet,HashMap}; extern crate regex; use regex::Regex; struct Card { id: usize,...

    Rust

    Day 4
    use std::env;
    use std::io::{self, prelude::*, BufReader};
    use std::fs::File;
    use std::collections::{HashSet,HashMap};
    
    extern crate regex;
    use regex::Regex;
    
    struct Card {
        id: usize,
        winning: HashSet<usize>,
        my_numbers: HashSet<usize>,
    }
    impl From<&String> for Card {
        fn from(s: &String) -> Self {
            let re = Regex::new(r"Card[\s]+(\d+): ([\d\s]+) \| ([\d\s]+)").unwrap();
            let matches = re.captures(&s).unwrap();
            Self {
                id:         matches[1].parse().unwrap(),
                winning:    matches[2].split_whitespace().map(|x| x.parse::<usize>().unwrap()).collect(),
                my_numbers: matches[3].split_whitespace().map(|x| x.parse::<usize>().unwrap()).collect(),
            }
        }
    }
    impl Card {
        pub fn matching_numbers(&self) -> usize {
            self.winning.iter().filter(|n| self.my_numbers.contains(n)).count()
        }
        pub fn score(&self) -> usize {
            let common = self.matching_numbers() as u32;
            if common == 0 { 0 } else { usize::pow(2,common-1) }
        }
    }
    
    fn solve(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 cards: Vec<_> = input.iter().map(Card::from).collect();
    
        // Part 1
        let part1: usize = cards.iter().map(|c| c.score()).sum();
        println!("Part 1: {part1}"); // 27454
    
        // Part 2
        let mut my_cards: HashMap<usize,usize> = HashMap::new();
        for card_id in 1..=cards.len() {
            my_cards.insert(card_id,1);
        }
        for card in cards {
            let count = *my_cards.get(&card.id).unwrap();
            for x in 1..=card.matching_numbers() {
                *my_cards.entry(&card.id+x).or_insert(0) += count;
            }
        }
        let part2: usize = my_cards.iter().map(|(_,v)| v).sum();
        println!("Part 2: {part2}"); // 6857330
    
        Ok(())
    }
    
    fn main() {
        let args: Vec<String> = env::args().collect();
        let filename = &args[1];
        solve(&filename).unwrap();
    }
    
    2 votes
  8. jzimbel
    Link
    My partner and I bought and decorated a Christmas tree for the first time in our adult lives last night, so I was a little too tired to stay up past midnight and solve this one quickly. So many...

    My partner and I bought and decorated a Christmas tree for the first time in our adult lives last night, so I was a little too tired to stay up past midnight and solve this one quickly. So many pine needles, everywhere!!

    Elixir

    Parts 1 and 2

    I think the thing I had the most trouble with on this one was naming, honestly.
    Part 2 seemed like it would be a challenge, but turned out to be quite straightforward once I came up with sensible—if a bit verbose—labels for the two variables that describe each card: {self_copy_count, child_copy_count}.

    It was nice that you could get away with not parsing any numbers for this one. It works fine just by comparing the numeric strings.
    You don't even need to use the card IDs!

    defmodule AdventOfCode.Solution.Year2023.Day04 do
      def part1(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_card/1)
        |> Enum.map(&score_card/1)
        |> Enum.sum()
      end
    
      def part2(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_card/1)
        |> count_cards()
      end
    
      defp score_card({_, 0}), do: 0
      defp score_card({_, n}), do: Integer.pow(2, n - 1)
    
      defp count_cards(cards, acc \\ 0)
      defp count_cards([], acc), do: acc
    
      defp count_cards([{self_copy_count, 0} | children], acc) do
        count_cards(children, acc + self_copy_count)
      end
    
      defp count_cards([{self_copy_count, child_copy_count} | children], acc) do
        {children_to_copy, unchanged} = Enum.split(children, child_copy_count)
    
        new_children =
          for {sub_self_copy_count, sub_child_copy_count} <- children_to_copy do
            {sub_self_copy_count + self_copy_count, sub_child_copy_count}
          end
    
        count_cards(new_children ++ unchanged, acc + self_copy_count)
      end
    
      defp parse_card(line) do
        [_card_number, contents] = String.split(line, ": ")
    
        [winners_str, hand_str] = String.split(contents, " | ")
        winners = MapSet.new(String.split(winners_str))
        hand = MapSet.new(String.split(hand_str))
    
        {1, MapSet.size(MapSet.intersection(winners, hand))}
      end
    end
    
    2 votes
  9. Crespyl
    Link
    This is one of those ones where understanding the instructions was the most complicated part. My Part 2 solution is slow, taking about 10 seconds to run on my machine, but that's good enough for...

    This is one of those ones where understanding the instructions was the most complicated part.

    My Part 2 solution is slow, taking about 10 seconds to run on my machine, but that's good enough for me.

    Part 1 - Ruby
    def parse_card(line)
      id = line.match(/Card\s+(\d+):.+/)[1].to_i
      numbers = line.split(":")[1].split("|").map { |s| s.split(" ").map(&:to_i) }
      {id: id,
       winning: numbers.first,
       have: numbers.last}
    end
    
    def score_card(card)
      matches = card[:winning].filter { |n| card[:have].include?(n) }
      return 0 if matches.empty?
      matches[1..].reduce(1) { |score, n| score * 2 }
    end
    
    def compute_p1(input)
      input
        .lines
        .map { |l| parse_card(l) }
        .map { |c| score_card(c) }
        .sum
    end
    
    Part 2
    def card_matches(card)
      card[:winning].filter { |n| card[:have].include?(n) }
    end
    
    def compute_p2(input)
      cards = input
                .lines
                .map { |l| parse_card(l) }
      copies_table = {}
      cards.each { |c| copies_table[c[:id]]=1 }
      cards.each_with_index do |card,idx|
        copies_table[idx+1].times do
          matches = card_matches(card).count
          1.upto(matches) do |i|
            copies_table[idx+i+1] += 1
          end
        end
      end
      copies_table.values.sum
    end
    
    1 vote
  10. Crestwave
    Link
    Feels more like a proper day 4 than the previous days did; simple and fun. I went the efficient route, although from @Crespyl's comment I'm guessing that it can be brute forced as well. Part 1...

    Feels more like a proper day 4 than the previous days did; simple and fun. I went the efficient route, although from @Crespyl's comment I'm guessing that it can be brute forced as well.

    Part 1
    #!/usr/bin/awk -f
    {
    	sub(/Card[ ]*[0-9]*: /, "")
    	split($0, card, "|")
    	card[2] = card[2] " "
    	$0 = card[1]
    	num = 0
    
    	for (i = 1; i <= NF; ++i)
    		if (match(card[2], "[^0-9]+" $i "[^0-9]+"))
    			num = num ? num * 2 : 1
    
    	total += num
    }
    
    END { print total }
    
    Part 2

    Due to past experience from Lanternfish and other exponential problems in the past, I decided to simply save their multipliers in a table instead of making actual copies of the cards.

    #!/usr/bin/awk -f
    {
    	mult[NR] += 1
    	sub(/Card[ ]*[0-9]*: /, "")
    	split($0, card, "|")
    	card[2] = card[2] " "
    	$0 = card[1]
    	num = 0
    
    	for (i = 1; i <= NF; ++i)
    		if (match(card[2], "[^0-9]+" $i "[^0-9]+"))
    			++num
    
    	for (i = 1; i <= num; ++i)
    		mult[NR + i] += 1 * mult[NR]
    
    	total += mult[NR]
    }
    
    END { print total }
    
    1 vote
  11. spit-evil-olive-tips
    Link
    part 1 useful corner of the stdlib I learned today: strings.Fields acts like strings.Split but automatically handles repeated whitespace for you. package day04 import ( "fmt" "math" "strconv"...
    part 1

    useful corner of the stdlib I learned today: strings.Fields acts like strings.Split but automatically handles repeated whitespace for you.

    package day04
    
    import (
        "fmt"
        "math"
        "strconv"
        "strings"
    
        "github.com/dlclark/regexp2"
        "github.com/spf13/cobra"
    
        "spit-evil-olive.tips/aoc2023/common"
    )
    
    type Card struct {
        ID      int
        Winners map[int]bool
        Choices map[int]bool
    }
    
    var cardRegex = regexp2.MustCompile(`Card +(?<id>\d+): (?<winners>[\d +]+) \| (?<choices>[\d ]+)`, regexp2.None)
    
    func parseNumbersToMap(s string) (map[int]bool, error) {
    	m := make(map[int]bool)
    
    	for _, strNumber := range strings.Fields(s) {
    		number, err := strconv.Atoi(strNumber)
    		if err != nil {
    			return m, fmt.Errorf("failed to parse '%s'", strNumber)
    		}
    		m[number] = true
    	}
    	
    	return m, nil
    }
    
    func parseInput(path string) ([]Card, error) {
        var cards []Card
    
        lines, err := common.ReadFileLines(path)
        if err != nil {
            return cards, err
        }
    
        for _, line := range lines {
            match, err := cardRegex.FindStringMatch(line)
            if err != nil {
                return cards, fmt.Errorf("failed to parse '%s': %w", line, err)
            }
    
            if match == nil {
                return cards, fmt.Errorf("failed to parse '%s'", line)
            }
    
            var card Card
    
            id := match.GroupByName("id").String()
            card.ID, err = strconv.Atoi(id)
            if err != nil {
                return cards, fmt.Errorf("failed to parse %s: %w", line, err)
            }
    
            card.Winners, err = parseNumbersToMap(match.GroupByName("winners").String())
            if err != nil {
                return cards, fmt.Errorf("failed to parse %s: %w", line, err)
            }
    
            card.Choices, err = parseNumbersToMap(match.GroupByName("choices").String())
            if err != nil {
                return cards, fmt.Errorf("failed to parse %s: %w", line, err)
            }
    
            cards = append(cards, card)
        }
    
        return cards, nil
    }
    
    func (c Card) Value() int {
        matches := 0
    
        for winner, _ := range c.Winners {
            if _, ok := c.Choices[winner]; ok {
                matches += 1
            }
        }
    
        if matches == 0 {
            return 0
        } else {
            return int(math.Pow(2.0, float64(matches-1)))
        }
    }
    
    var CommandA = &cobra.Command{
        Use:  "04a",
        Args: cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            cards, err := parseInput(args[0])
            if err != nil {
                return err
            }
    
            sum := 0
            
            for _, card := range cards {
                sum += card.Value()
            }
    
            fmt.Printf("%v\n", sum)
    
            return nil
        },
    }
    
    part 2

    I refactored my Value() function to allow MatchCount() as well:

    --- day04a.go   2023-12-03 21:54:42.345171158 -0800
    +++ day04b.go   2023-12-03 21:55:01.321394030 -0800
    @@ -75,19 +75,24 @@
            return cards, nil
     }
    
    -func (c Card) Value() int {
    -       matches := 0
    +func (c Card) MatchCount() int {
    +       count := 0
    
            for winner, _ := range c.winners {
                    if _, ok := c.choices[winner]; ok {
    -                       matches += 1
    +                       count += 1
                    }
            }
    
    -       if matches == 0 {
    +       return count
    +}
    +
    +func (c Card) Value() int {
    +       count := c.MatchCount()
    +       if count == 0 {
                    return 0
            } else {
    -               return int(math.Pow(2.0, float64(matches-1)))
    +               return int(math.Pow(2.0, float64(count-1)))
            }
     }
    

    and then it's pretty simple to keep track of card counts:

    var CommandB = &cobra.Command{
        Use:  "04b",
        Args: cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            cards, err := parseInput(args[0])
            if err != nil {
                return err
            }
    
            cardCounts := make(map[int]int)
    
            for _, card := range cards {
                cardCounts[card.ID] += 1
                count := cardCounts[card.ID]
    
                for i := 1; i <= card.MatchCount(); i++ {
                    cardCounts[card.ID+i] += count
                }
            }
    
            totalCount := 0
            for _, count := range cardCounts {
                totalCount += count
            }
    
            fmt.Printf("%v\n", totalCount)
    
            return nil
        },
    }
    
    performance
    $ time ./aoc2023 04a day04/04.txt > /dev/null
    
    ________________________________________________________
    Executed in    3.65 millis    fish           external
       usr time    1.75 millis  306.00 micros    1.44 millis
       sys time    2.30 millis  138.00 micros    2.16 millis
    
    > time ./aoc2023 04b day04/04.txt > /dev/null
    
    ________________________________________________________
    Executed in    3.81 millis    fish           external
       usr time    0.71 millis  233.00 micros    0.47 millis
       sys time    3.90 millis  109.00 micros    3.79 millis
    
    1 vote
  12. scarecrw
    Link
    I'm taking this year as an opportunity to try something new and using Haskell. Really enjoying it so far, though I need to check out how regex works as my current parsing could be improved. After...

    I'm taking this year as an opportunity to try something new and using Haskell. Really enjoying it so far, though I need to check out how regex works as my current parsing could be improved. After going for the leaderboard in years past with python, just messing about has been much more fun. If you have any Haskell tips I could benefit from, let me know!

    Solution
    data Card = Card {
        cardNumber :: Int,
        winningNums :: [Int],
        yourNums :: [Int]
    } deriving (Show)
    
    parseCard :: String -> Card
    parseCard s = Card cardNum winning your where
        cardNum = read $ take 4 $ drop 4 s
        winning = map read $ drop 2 $ words (takeWhile (/='|') s)
        your = map read (tail (words $ dropWhile (/='|') s))
    
    scoreCard :: Card -> Int
    scoreCard (Card _ winning yours) = length (filter (`elem` winning) yours)
    
    solve1 :: [String] -> Int
    solve1 input = sum $ map (points . scoreCard . parseCard) input where
        points n = if n == 0 then 0 else 2 ^ (n - 1)
    
    solve2 :: [String] -> Int
    solve2 input = score (map (scoreCard . parseCard) input) [1,1..] 0 where
        score [] _ acc = acc
        score (x:xs) (c:count) acc = score xs count' acc' where
            acc' = acc + c
            count' = map (+c) (take x count) ++ drop x count
    
    1 vote
  13. pnutzh4x0r
    Link
    Language: Python Part 1 Sets really came in handy for this challenge, as did recognizing that you can use powers of two to compute the points for each card. I tried using a regular expression to...

    Language: Python

    Part 1

    Sets really came in handy for this challenge, as did recognizing that you can use powers of two to compute the points for each card. I tried using a regular expression to parse each card, but ended up just doing it manually with split :|

    Numbers = set[int]
    Card    = list[Numbers]
    
    def read_cards(stream=sys.stdin) -> Iterator[Card]:
        for line in stream:
            yield [set(map(int, n.split())) for n in line.split(':')[-1].split('|')]
    
    def main(stream=sys.stdin) -> None:
        cards  = [numbers & winning for winning, numbers in read_cards(stream)]
        points = sum(2**(len(card)-1) for card in cards if card)
        print(points)
    
    Part 2

    This took me longer than I wished... I had to think about it carefully before seeing how you can just keep track of the counts of each card, and then when you get to that card, you add to its copies your current count.

    def main(stream=sys.stdin) -> None:
        cards  = [numbers & winning for winning, numbers in read_cards(stream)]
        counts = defaultdict(int)
    
        for index, card in enumerate(cards, 1):
            counts[index] += 1
            for copy in range(index + 1, index + len(card) + 1):
                counts[copy] += counts[index]
    
        print(sum(counts.values()))
    

    GitHub Repo

    1 vote
  14. csos95
    Link
    As others have said, it's a pretty simple one today. I redid part 1 of day 1 with nom and then decided not to bother with it any further if I don't have to. I had forgotten how annoyingly opaque...

    As others have said, it's a pretty simple one today.
    I redid part 1 of day 1 with nom and then decided not to bother with it any further if I don't have to.
    I had forgotten how annoyingly opaque its errors are.

    Rust
    use regex::Regex;
    use std::collections::{HashSet, VecDeque};
    
    #[aoc(day4, part1)]
    fn part1(input: &str) -> usize {
        let number_re = Regex::new("([0-9]+)").unwrap();
        input
            .lines()
            .map(|line| {
                let (_card, matches) = number_re
                    .captures_iter(line)
                    .skip(1)
                    .map(|c| c.get(1).unwrap().as_str())
                    .enumerate()
                    .fold((HashSet::new(), 0), |(mut card, mut matches), (i, n)| {
                        if i < 10 {
                            card.insert(n);
                        } else if card.contains(n) {
                            matches += 1;
                        }
                        (card, matches)
                    });
    
                if matches == 0 {
                    0
                } else {
                    2usize.pow(matches - 1)
                }
            })
            .sum()
    }
    
    #[aoc(day4, part2)]
    fn part2(input: &str) -> usize {
        let number_re = Regex::new("([0-9]+)").unwrap();
        let card_matches: Vec<usize> = input
            .lines()
            .map(|line| {
                let (_card, matches) = number_re
                    .captures_iter(line)
                    .skip(1)
                    .map(|c| c.get(1).unwrap().as_str())
                    .enumerate()
                    .fold((HashSet::new(), 0), |(mut card, mut matches), (i, n)| {
                        if i < 10 {
                            card.insert(n);
                        } else if card.contains(n) {
                            matches += 1;
                        }
                        (card, matches)
                    });
                matches
            })
            .collect();
    
        let mut processing_cards = VecDeque::new();
    
        for (i, _) in card_matches.iter().enumerate() {
            processing_cards.push_back(i);
        }
    
        let mut total_cards = 0;
        while !processing_cards.is_empty() {
            let card_i = processing_cards.pop_front().unwrap();
            let matches = card_matches[card_i];
            for i in 0..matches {
                processing_cards.push_back(card_i + i + 1);
            }
            total_cards += 1;
        }
    
        total_cards
    }
    
    1 vote
  15. fxgn
    Link
    Language: Julia Today was surprisingly simple for some reason, especially compared to yesterday Solution julia> input = readlines("Downloads/input"); julia> a = 0; julia> for card in input data =...

    Language: Julia
    Today was surprisingly simple for some reason, especially compared to yesterday

    Solution
    julia> input = readlines("Downloads/input");
    
    julia> a = 0;
    
    julia> for card in input
               data = split(card, ": ")[2]
               win, my = split.(split(data, " | "))
               value = 0
               for number in my
                   if number  win
                       if value == 0
                           value = 1
                       else
                           value *= 2
                       end
                   end
               end
               a += value
           end
    
    julia> a
    20667
    
    julia> counts = Dict(i => 1 for i in 1:length(input));
    
    julia> for (i, card) in enumerate(input)
               data = split(card, ": ")[2]
               win, my = split.(split(data, " | "))
               value = 0
               for number in my
                   if number  win
                       value += 1
                   end
               end
               if value > 0
                   for cn in i+1:i+value
                       counts[cn] += counts[i]
                   end
               end
           end
    
    julia> counts |> values |> sum
    5833065
    

    The code could've probably been shorter if I used maps and generators and other functional trickery, but whatever, it's short enough as it is.

    1 vote
  16. Toric
    Link
    Im really coming to love nom for parsing. Also really proud of my solution performance wise, I only have to solve each card once, dont have to solve any of its copies....

    Im really coming to love nom for parsing. Also really proud of my solution performance wise, I only have to solve each card once, dont have to solve any of its copies.

    https://github.com/gabevenberg/advent-of-code-2023/tree/main/days/day04

    1 vote
  17. akk
    Link
    Dayjob made me put this off until after work and other responsibilities. Kinda messy. Part 2 had me stumped for a while. I conceptually knew what to do, but couldn't figure out what to type into...

    Dayjob made me put this off until after work and other responsibilities. Kinda messy. Part 2 had me stumped for a while. I conceptually knew what to do, but couldn't figure out what to type into IntelliJ.

    Java Solution
    package com.michaeleisemann.aoc23.services;
    
    import com.michaeleisemann.aoc23.interfaces.DayInterface;
    import org.springframework.stereotype.Service;
    
    import java.util.*;
    
    // https://adventofcode.com/2023/day/4
    @Service
    public class Day4Service implements DayInterface {
    
        private class Card {
            private record Numbers (List<Integer> numbers) {
                public Numbers(String numbers) {
                    // there can be more than one space between numbers
                    this(Arrays.stream(numbers.split("\\s+")).map(Integer::parseInt).toList());
                }
            }
    
            private final int id;
    
            private final Numbers winningNumbers;
            private final Numbers actualNumbers;
    
            public Card(int id, String winningNumbers, String actualNumbers) {
                this.id = id;
                this.winningNumbers = new Numbers(winningNumbers);
                this.actualNumbers = new Numbers(actualNumbers);
            }
    
            public int getId() {
                return id;
            }
    
            public Numbers getWinningNumbers() {
                return winningNumbers;
            }
    
            public Numbers getActualNumbers() {
                return actualNumbers;
            }
    
            /**
             * Find which actual numbers are in the winning numbers
             * @return
             */
            public List<Integer> getMatchingNumbers() {
                List<Integer> matchingNumbers = new ArrayList<>();
                for (Integer actualNumber : actualNumbers.numbers) {
                    if (winningNumbers.numbers.contains(actualNumber)) {
                        matchingNumbers.add(actualNumber);
                    }
                }
                return matchingNumbers;
            }
    
            @Override
            public String toString() {
                return "Card{" +
                        "id=" + id +
                        ", winningNumbers=" + winningNumbers +
                        ", actualNumbers=" + actualNumbers +
                        '}';
            }
    
        }
    
    
        public String part1(String puzzleInput) {
            List<Card> cards = parsePuzzleInput(puzzleInput);
            int total = 0;
            for (Card card : cards) {
                List<Integer> matchingNumbers = card.getMatchingNumbers();
                if (matchingNumbers.isEmpty()) {
                    continue;
                }
                /* The first match makes the card worth one point and each match after the first doubles the point value of that card. */
                int points = 1;
                for (int i = 1; i < matchingNumbers.size(); i++) {
                    points *= 2;
                }
                total += points;
    
            }
            return String.valueOf(total);
        }
    
        public String part2(String puzzleInput) {
            List<Card> cards = parsePuzzleInput(puzzleInput);
            HashMap<Integer, Integer> cardCopies = new HashMap<>();
    
            // Add all the cardIds to the map with a value of 1 (base)
            for (Card card : cards) {
                cardCopies.put(card.getId(), 1);
            }
    
            for (int i = 0; i < cards.size(); i++) {
                Card card = cards.get(i);
                int copies = cardCopies.get(card.getId());
                int newCards = card.getMatchingNumbers().size();
                for (int j = 1; j <= newCards && i + j < cards.size(); j++) {
                    cardCopies.put(cards.get(i + j).getId(), cardCopies.getOrDefault(cards.get(i + j).getId(), 0) + copies);
                }
            }
    
            int totalCards = cardCopies.values().stream().mapToInt(Integer::intValue).sum();
            return String.valueOf(totalCards);
        }
    
    
        private List<Card> parsePuzzleInput(String puzzleInput) {
            List<Card> cards = new ArrayList<>();
            for (String line : puzzleInput.split("\n")) {
                // Split on the first colon
                String[] parts = line.split(":", 2);
                String id = parts[0].replaceAll("\\D+", "");
                String[] numbers = parts[1].split("\\|");
                cards.add(new Card(Integer.parseInt(id), numbers[0].trim(), numbers[1].trim()));
            }
            return cards;
        }
    
        private Card findCardById(List<Card> cards, int id) {
            for (Card card : cards) {
                if (card.getId() == id) {
                    return card;
                }
            }
            return null;
        }
    }
    
    1 vote
  18. whispersilk
    Link
    Rust Data structures and parser #[derive(Debug)] struct Card { winners: usize, } impl Card { fn num_winners(&self) -> usize { self.winners } fn score(&self) -> u32 { 1 << self.winners >> 1 } }...

    Rust

    Data structures and parser
    #[derive(Debug)]
    struct Card {
        winners: usize,
    }
    
    impl Card {
        fn num_winners(&self) -> usize {
            self.winners
        }
    
        fn score(&self) -> u32 {
            1 << self.winners >> 1
        }
    }
    
    impl TryFrom<&str> for Card {
        type Error = Box<dyn std::error::Error>;
        fn try_from(value: &str) -> Result<Self> {
            type Res<T> = std::result::Result<Vec<T>, std::num::ParseIntError>;
    
            let (_, data) = value.split_once(':').ok_or("Line is missing card id")?;
            let (cards, have) = data.split_once('|').ok_or("Line is missing '|'")?;
            let cards: Vec<_> = cards
                .trim()
                .split(' ')
                .filter(|s| s.len() > 0)
                .map(|n| n.trim().parse::<u32>())
                .collect::<Res<_>>()?;
            let winners = have
                .trim()
                .split(' ')
                .filter(|s| s.len() > 0)
                .map(|n| n.trim().parse::<u32>().map(|val| cards.iter().find(|v| **v == val)))
                .try_fold(0, |acc, x| x.map(|opt| acc + opt.map(|_| 1).unwrap_or(0)))?;
            Ok(Card { winners })
        }
    }
    
    Part 1
    fn first() -> Result<String> {
        let summed_scores = load_input(4)?
            .lines()
            .map(Card::try_from)
            .try_fold(0, |acc, x| x.map(|card| acc + card.score()))?
            .to_string();
        Ok(summed_scores)
    }
    
    Part 2
    fn second() -> Result<String> {
        let cards = load_input(4)?
            .lines()
            .map(Card::try_from)
            .collect::<Result<Vec<Card>>>()?;
        let num_cards = cards.len();
        let mut copies = vec![1; num_cards];
        for (idx, card) in cards.iter().enumerate() {
            let curr_count = copies[idx];
            let num_to_boost = card.num_winners();
            for i in (idx + 1)..=(std::cmp::min(idx + num_to_boost, num_cards)) {
                copies[i] += curr_count;
            }
        }
        let total_copies = copies.iter().sum::<u32>().to_string();
        Ok(total_copies)
    }
    

    What I've got so far is up on GitHub.

    1 vote
  19. updawg
    Link
    Guess I shouldn't have started when it was already almost time for the next challenge to come out... Attempt 1 with open("input.txt", 'r', encoding="utf-8") as file: data =...

    Guess I shouldn't have started when it was already almost time for the next challenge to come out...

    Attempt 1
    with open("input.txt", 'r', encoding="utf-8") as file:
        data = file.read().strip().split('\n')
    
    data = [x.split(':') for x in data]
    data = [x[1].split('|') for x in data]
    data = [[x[0].split(), x[1].split()] for x in data]
    
    
    def p1(x):
        points = 0
        for card in x:
            matches = 0 # Number of matches on this card
            for i in card[0]:
                if i in card[1]:
                    matches += 1
            points += 2**(matches-1) if matches > 0 else 0
        return points
    
    def p2(x):
        scratchies = list(range(1,202)) # We initially have one copy of each card 1-201
        current_card = 0
        for card in x:
            current_card += 1
            current_card_copies = scratchies.count(current_card) # Number of total copies of current card
            matches = 0 # Matches on this card
            for i in card[0]:
                if i in card[1]:
                    matches += 1
            for j in list(range(1, matches + 1)):
                # We need to append [current_card+j] once per copy of the current card
                scratchies.extend([current_card + j] * current_card_copies)
        return len(scratchies)
    
    print(f"{p1(data)}\n{p2(data)}")
    
    Optimization
    from collections import Counter # pylint: disable=C0415
    
    with open("input.txt", 'r', encoding="utf-8") as file:
        data = file.read().strip().split('\n')
    data = [x.split(':') for x in data]
    data = [x[1].split('|') for x in data]
    data = [[x[0].split(), x[1].split()] for x in data]
    
    points = 0
    for card in data:
        matches = 0 # Number of matches on this card (reset to 0 each loop)
        matches = sum(1 for i in card[0] if i in card[1]) # Add 1 to matches for each # in card[0] if it's in card[1]
        points += 2**(matches-1) if matches > 0 else 0 # Add 2^(# of matches -1) points (assuming there are any matches)
    
    scratchies = Counter(range(1,202)) # We initially have one copy of each card 1-201
    current_card = 0
    total_scratchies = 0
    for card in data:
        current_card += 1 # Move to the next card number
        current_copies = scratchies[current_card] # Number of total copies of current card
        matches = 0 # Matches on this card
        matches = sum(1 for i in card[0] if i in card[1]) # Add 1 to matches for each # in card[0] if it's in card[1]
        for j in range(1, matches + 1): # Add claimed card numbers to the list of cards once for each copy of the current card
            scratchies[current_card + j] += current_copies
        total_scratchies += scratchies[current_card]
    
    print(f"Part 1: {points}\nPart 2: {total_scratchies}")
    

    My optimized version runs ~2000 times faster than my initial version, so that's nice.

    1 vote