12 votes

Day 2: Gift Shop

Today's problem description: https://adventofcode.com/2025/day/2

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>

21 comments

  1. lily
    Link
    To be honest, I didn't really know what I was doing with this one. Still don't have a clue how to do it in a "smart" way. I just bruteforced it, and it takes a couple seconds to run, which I'm not...

    To be honest, I didn't really know what I was doing with this one. Still don't have a clue how to do it in a "smart" way. I just bruteforced it, and it takes a couple seconds to run, which I'm not super happy with, but I'll take the win for tonight. Maybe I'll come back to it in the morning and see if I can figure out a nicer solution. I'm doing a lot of string copying right now, which I think is the main source of overhead.

    Solution (Lily)
    var invalid_sum_half_width = 0
    var invalid_sum_any_width = 0
    
    for range in File.read_to_string("inputs/day_02.txt").split(",").map(|string|
        string.split("-").map(String.parse_i).map(Option.unwrap)
    ): {
        for id in range[0]...range[1]: {
            var string = id.to_s()
            var size = string.size()
            var half_width = size / 2
    
            if string.slice(0, half_width) == string.slice(half_width): {
                invalid_sum_half_width += id
                invalid_sum_any_width += id
                continue
            }
    
            for width in 1...size: {
                if size % width: {
                    continue
                }
    
                var left = 0
                var right = width
                var previous = string.slice(left, right)
                var invalid = true
    
                do: {
                    left += width
                    right += width
    
                    var current = string.slice(left, right)
    
                    if current != previous: {
                        invalid = false
                        break
                    }
    
                    previous = current
                } while right < size
    
                if invalid: {
                    invalid_sum_any_width += id
                    break
                }
            }
        }
    }
    
    print("Part 1: {}\nPart 2: {}".format(
        invalid_sum_half_width, invalid_sum_any_width
    ))
    
    4 votes
  2. scarecrw
    Link
    Brute-forced it again, but learned a lot in doing so! Had one of my first "aha!" moments in using string_concat/3 backwards to both check if one string was a prefix of another and find the suffix...

    Brute-forced it again, but learned a lot in doing so!

    Had one of my first "aha!" moments in using string_concat/3 backwards to both check if one string was a prefix of another and find the suffix that matched. No idea if it's the most efficient approach, but super neat!

    Prolog Solution
    parse_range(RangeString, Range):-
        split_string(RangeString, "-", "", RangeStrings),
        maplist(number_string, Range, RangeStrings).
    
    find_invalids(IsInvalid, [RangeStart, RangeEnd], Invalids):-
        numlist(RangeStart, RangeEnd, Nums),
        include(IsInvalid, Nums, Invalids).
    
    is_invalid_1(Num):-
        number_string(Num, NumString),
        string_length(NumString, L),
        PartLength is div(L, 2),
        sub_string(NumString, 0, PartLength, PartLength, Left),
        sub_string(NumString, PartLength, PartLength, 0, Right),
        Left == Right.
    
    is_invalid_2(Num):-
        number_string(Num, NumString),
        string_length(NumString, L),
        between(2, L, NumParts),
        0 is mod(L, NumParts),
        Before is div(L, NumParts),
        sub_string(NumString, 0, Before, _, Part),
        string_repeat(Part, NumString).
    
    string_repeat(_, "").
    string_repeat(Part, String):-
        string_concat(Part, Rest, String),
        string_repeat(Part, Rest).
    
    main:-
        Filename = 'inputs/day02.txt',
        read_file_to_string(Filename, Input, []),
        split_string(Input, ",", "", RangeStrings),
        maplist(parse_range, RangeStrings, Ranges),
    
        maplist(find_invalids(is_invalid_1), Ranges, Invalids),
        maplist(sum_list, Invalids, InvalidSums),
        sum_list(InvalidSums, Result1),
        write("Part 1: "), write(Result1), nl,
    
        maplist(find_invalids(is_invalid_2), Ranges, Invalids2),
        maplist(sum_list, Invalids2, InvalidSums2),
        sum_list(InvalidSums2, Result2),
        write("Part 2: "), write(Result2), nl,
    
        halt.
    
    :- initialization(main).
    

    I made a note to learn about records, as right now I'm just using lists for everything.

    3 votes
  3. aeriforms
    Link
    This is good exercise for me to brush up on python again (as unemployed cs grad). It's surprisingly neat when it comes to data transformations using list comprehesions and method chaining. The...

    This is good exercise for me to brush up on python again (as unemployed cs grad). It's surprisingly neat when it comes to data transformations using list comprehesions and method chaining. The task of repeating strings lends well to regex too which I'm kinda trigger happy to use. Anyhow, this is my first time doing AoC and I'm having a good time so far, despite forgetting basic stuff that I have to ChatGPT my way through like reading a file. Combine with type declarations and pyright/basedpyright and troubleshooting is really fast.

    Solution
    import re
    Range = tuple[int, int]
    range_tuples: list[Range]
    pattern = re.compile(r"^(.+)\1+$") #substring, repeated at least once
    
    def main():
      with open("day2-ids.txt", "r") as file:
        range_strings: list[str] = file.readline().split(",")
        range_tuples = [(int(start), int(end)) for s in range_strings for start, end in [s.split("-")]]
    
      sum_of_invalid_ids: int = 0
    
      for start, end in range_tuples:
        for i in range(start, end+1):
          string_i = str(i)
          if bool(pattern.match(string_i)):
            sum_of_invalid_ids += i
    
      print(sum_of_invalid_ids)
    
    if __name__ == "__main__":
      main()
    
    3 votes
  4. tomf
    (edited )
    Link
    This is only part one. I'll do the second later. Google Sheets =INDEX( LET( _i,SPLIT(TOCOL(SPLIT(A1,",")),"-"), _n,--REPT(ROW(1:99999),2), SUM( SPLIT( BYROW( _i, LAMBDA( _x, LET( _l,INDEX(_x,0,1),...

    This is only part one. I'll do the second later.

    Google Sheets
    =INDEX(
      LET(
       _i,SPLIT(TOCOL(SPLIT(A1,",")),"-"),
       _n,--REPT(ROW(1:99999),2),
       SUM(
        SPLIT(
         BYROW(
          _i,
          LAMBDA(
           _x,
           LET(
            _l,INDEX(_x,0,1),
            _r,INDEX(_x,0,2),
            JOIN(
             ",",
             IFERROR(
              QUERY(
               _n,
               "where
                 Col1 >="&_l&" and 
                 Col1 <="&_r&""),
              ","))))),
        ","))))
    

    edit: faster

    2 votes
  5. netsplit
    (edited )
    Link
    Here are my solutions for both parts in Perl using a feature from the relatively newly released v5.42. I always find that I lean a little heavily on regular expressions for my Advent of Code...

    Here are my solutions for both parts in Perl using a feature from the relatively newly released v5.42. I always find that I lean a little heavily on regular expressions for my Advent of Code solutions.

    Part 1 (Perl)
    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    
    use v5.42.0;
    
    use feature 'keyword_all';
    no warnings 'experimental::keyword_all';
    
    use File::Slurper 'read_lines';
    
    my @content = read_lines 'input.txt';
    my @ranges  = split ',', $content[0];
    my $total   = 0;
    
    foreach my $range (@ranges) {
        my ($start, $end) = split '-', $range;
    
        foreach my $id ($start..$end) {
            my @chars  = split //, $id;
            my $digits = scalar @chars;
    
            next if $digits % 2 != 0;
    
            my $count = int $digits / 2;
            my @parts = $id =~ /\d{$count}/g;
    
            $total += $id if scalar @parts == 2 && $parts[0] == $parts[1];
        }
    }
    
    print "The answer is: ${total}\n";
    
    Part 2 (Perl)
    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    
    use v5.42.0;
    
    use feature 'keyword_all';
    no warnings 'experimental::keyword_all';
    
    use File::Slurper 'read_lines';
    
    my @content = read_lines 'input.txt';
    my @ranges  = split ',', $content[0];
    my $total   = 0;
    
    foreach my $range (@ranges) {
        my ($start, $end) = split '-', $range;
    
        ID: foreach my $id ($start..$end) {
            my @chars  = split //, $id;
            my $digits = scalar @chars;
    
            next if $digits < 2;
    
            for (my $i = 2; $i <= $digits; ++$i) {
                next if $digits % $i != 0;
    
                my $count = int $digits / $i;
                my @parts = $id =~ /\d{$count}/g;
    
                if (scalar @parts == $i && all { $_ eq $parts[0] } @parts) {
                    $total += $id;
    
                    next ID;
                }
            }
        }
    }
    
    print "The answer is: ${total}\n";
    

    Slightly compacted versions...

    Part 1 (Perl)
    perl -ne 'for(split/,/){($s,$e)=split/-/;for($s..$e){$t+=$_ if/^(.*)\1$/}}print$t' input.txt
    
    Part 2 (Perl)
    perl -ne 'for(split/,/){($s,$e)=split/-/;for($s..$e){$t+=$_ if/^(.*)\1+$/}}print$t' input.txt
    
    2 votes
  6. [2]
    jonah
    Link
    I didn't post yesterday because I was annoyed at how long it took me. I was much happier today. My part 1 took very little time to write and executed pretty quickly, but part 2 took a little...

    I didn't post yesterday because I was annoyed at how long it took me. I was much happier today. My part 1 took very little time to write and executed pretty quickly, but part 2 took a little longer. On my machine I averaged about 1.2s. I decided to plug in some threading (not included here) to see if I could get it faster, and got it down to ~230ms. That was a cool win for today!

    Part 1
    from common import load_input
    input = load_input()
    
    # There's gotta be a better way to do this
    def id_is_invalid(prod_id: int) -> bool:
        str_id = str(prod_id)
        if len(str_id) % 2 != 0:
            return False
        mid = int(len(str_id) / 2)
        first, last = str_id[:mid], str_id[mid:]
        return first == last
    
    # Gross, but it works
    ranges = list(map(lambda x: list(map(lambda y: int(y), x.split('-'))), input.split(',')))
    counter = 0
    
    # Dumb solution but it works
    for r in ranges:
        for prod_id in range(r[0], r[1] + 1):
            if id_is_invalid(prod_id):
                counter += prod_id
    
    print(counter)
    
    Part 2
    from common import load_input
    input = load_input()
    
    def has_pattern_count(s, m, k):
        if s.count(s[:m]) >= k:
            return True
        return False
    
    def id_is_invalid(prod_id: int) -> bool:
        # Single digit IDs can't possibly match the criteria
        # And yes, this got me
        if prod_id < 10:
            return False
    
        str_id = str(prod_id)
        str_len = len(str_id)
    
        # m -> pattern length
        # k -> number of repetitions
        # m * k should always equal str_len
        # i.e., str_len / m should always be a whole number, and the result is k
    
        m = 1
        while m < (str_len / 2) + 1:
            if str_len % m == 0:
                k = str_len / m
                if has_pattern_count(str_id, m, k):
                    return True
            m += 1
    
        return False
    
    # Gross, but it works
    ranges = list(map(lambda x: list(map(lambda y: int(y), x.split('-'))), input.split(',')))
    counter = 0
    
    # Dumb solution but it works
    for r in ranges:
        print("Processing range:", r)
        for prod_id in range(r[0], r[1] + 1):
            if id_is_invalid(prod_id):
                counter += prod_id
    
    print(counter)
    
    2 votes
    1. thecakeisalime
      Link Parent
      I had a very similar solution to you, also in python. Seems like the big difference is that you're iterating from smaller to larger chunk sizes, and I'm iterating from halving through to 1/n-ing....

      I had a very similar solution to you, also in python. Seems like the big difference is that you're iterating from smaller to larger chunk sizes, and I'm iterating from halving through to 1/n-ing. I think they're roughly equivalent in efficiency, but yours seems more intuitive. My only advantage is that it avoids the single digit issue you ran into.

      I'm not super happy with this algorithm, but it seems like everyone has done pretty much the same thing, so at least I'm not alone.

      Part 1
      with open("20250102.in", "r") as file:
        ids = file.readline().split(',')
        total = 0
      
        for id in ids:
          ranges = id.split('-')
          start = int(ranges[0])
          end = int(ranges[1])
      
          for i in range(start, end + 1):
            s = str(i)
            if len(s) % 2 != 0:
              continue
            firstHalf = s[:len(s)//2]
            secondHalf = s[len(s)//2:]
      
            if firstHalf == secondHalf:
              total += i
      
        print(total)
      
      Part 2
      with open("20250102.in", "r") as file:
        ids = file.readline().split(',')
        total = 0
        invalidIds = []
      
        def hasPattern(s, chunkSize, numChunks):
          return s.count(s[:chunkSize]) == numChunks
      
        for id in ids:
          ranges = id.split('-')
          start = int(ranges[0])
          end = int(ranges[1])
      
          for i in range(start, end + 1):
            s = str(i)
      
            for n in range(2, len(s)+1):
              if len(s) % n == 0:
                if hasPattern(s, int(len(s)/n), n):
                  total += i
                  invalidIds.append(i)
                  break
      
        print(total)
        #print(invalidIds)
      
      2 votes
  7. [2]
    jzimbel
    (edited )
    Link
    Elixir I did my best to optimize my part 1 solution by filtering out large chunks of the ID ranges that would not work. (hint: the ID needs to be able to split into two parts with equal numbers of...

    Elixir

    I did my best to optimize my part 1 solution by filtering out large chunks of the ID ranges that would not work. (hint: the ID needs to be able to split into two parts with equal numbers of digits.)

    But none of that optimization really applies to part 2, so I did basically a brute-force approach for it. It took over a second to complete before I tweaked it to process all rows concurrently using Task.async_stream/3. Running concurrently, it takes about half a second.

    Parsing the input
    use AdventOfCode.Solution.SharedParse
    
    @impl true
    def parse(input) do
      input
      |> String.trim()
      |> String.split(",")
      |> Enum.map(&parse_range/1)
    end
    
    defp parse_range(str) do
      ~r/(\d+)-(\d+)/
      |> Regex.run(str, capture: :all_but_first)
      |> Enum.map(&String.to_integer/1)
      |> then(fn [first, last] -> first..last//1 end)
    end
    
    Part 1
    import Integer, only: [is_odd: 1]
    
    def part1(ranges) do
      ranges
      |> Task.async_stream(&(&1 |> invalid_ids_p1() |> Enum.sum()), ordered: false)
      |> Enum.sum_by(fn {:ok, sum} -> sum end)
    end
    
    defp invalid_ids_p1(range) do
      range = clamp_range(range)
      exponent = exponent_of_10(range.first)
      splitter = Integer.pow(10, div(exponent, 2) + 1)
      Stream.filter(range, &(div(&1, splitter) == rem(&1, splitter)))
    end
    
    # Shrinks the range so that it contains only numbers with even numbers of digits.
    defp clamp_range(first..last//1) do
      clamp_up(first)..clamp_down(last)//1
    end
    
    # Note: In my input, none of the ranges are so large that they include numbers
    # within multiple odd exponents of 10, e.g. 10-1000.
    #
    # So clamping the single range is enough, ranges never need to be split into
    # multiple sub-ranges to remove even exponents of 10 in the middle.
    
    defp clamp_up(n) do
      exponent = exponent_of_10(n)
      if is_odd(exponent), do: n, else: Integer.pow(10, exponent + 1)
    end
    
    defp clamp_down(n) do
      exponent = exponent_of_10(n)
      if is_odd(exponent), do: n, else: Integer.pow(10, exponent) - 1
    end
    
    defp exponent_of_10(n), do: floor(:math.log10(n))
    
    Part 2
    def part2(ranges) do
      ranges
      |> Task.async_stream(&(&1 |> invalid_ids_p2() |> Enum.sum()), ordered: false)
      |> Enum.sum_by(fn {:ok, sum} -> sum end)
    end
    
    defp invalid_ids_p2(range) do
      Stream.filter(range, &invalid_p2?/1)
    end
    
    defp invalid_p2?(n) do
      digits = Integer.digits(n)
      len = length(digits)
    
      # Generate chunk sizes to try splitting the digits up into
      1..div(len, 2)//1
      # Chunk size must divide the digits cleanly with no smaller leftover chunk at the end
      |> Stream.filter(&(div(len, &1) == len / &1))
      |> Enum.any?(fn chunk_size ->
        digits
        |> Enum.chunk_every(chunk_size)
        |> Enum.uniq()
        |> length()
        |> Kernel.==(1)
      end)
    end
    
    Benchmarks

    Without concurrency:

    Name             ips        average  deviation         median         99th %
    Parse       32028.96      0.00003 s    ±23.95%      0.00003 s      0.00007 s
    Part 1        118.94      0.00841 s     ±1.76%      0.00835 s      0.00875 s
    Part 2          0.52         1.91 s     ±0.99%         1.92 s         1.92 s
    

    With concurrency:

    Name             ips        average  deviation         median         99th %
    Parse       32601.48      0.0307 ms    ±21.58%      0.0284 ms      0.0701 ms
    Part 1        331.88        3.01 ms     ±6.84%        3.02 ms        3.46 ms
    Part 2          2.02      496.24 ms     ±1.39%      495.07 ms      510.70 ms
    
    2 votes
    1. hpr
      Link Parent
      When scanning your solution, there's definitely some standard-library stuff that I hadn't stumbled upon yet, that I maybe should be using. I was a little unhappy with the runtime, but async_stream...

      When scanning your solution, there's definitely some standard-library stuff that I hadn't stumbled upon yet, that I maybe should be using.
      I was a little unhappy with the runtime, but async_stream was surprisingly approachable and cut it down to ~2 seconds for both tasks (which still isn't exactly fast).

      I reckon doing the splitting on integers instead of strings might be the next obvious thing to improve, but I can't be bothered to do more now, time to sleep.

      Code
      defmodule DayTwo do
        @moduledoc """
        Day Two of AOC
        """
        require Integer
      
        def is_invalid_id_part_one(id) when is_number(id) do
          is_invalid_id_part_one(Integer.to_string(id))
        end
      
        def is_invalid_id_part_one(id) when is_binary(id) do
          length = String.length(id)
      
          if rem(length, 2) == 0 do
            is_invalid_id_with_repeat_length(id, 2)
          else
            false
          end
        end
      
      
        def is_invalid_id_part_two(id) when is_number(id) do
          is_invalid_id_part_two(Integer.to_string(id))
        end
      
        def is_invalid_id_part_two(id) when is_binary(id) do
          length = String.length(id)
      
          1..length
          |> Enum.map(fn len -> is_invalid_id_with_repeat_length(id, len) end)
          |> Enum.any?()
        end
      
        def is_invalid_id_with_repeat_length(id, len) do
          length = String.length(id)
      
          if rem(length, len) == 0 do
            [first_part | parts] = split_multiple(id, div(length, len))
            Enum.any?(parts) and Enum.all?(parts, &(first_part === &1))
          else
            false
          end
        end
      
        def split_multiple(str, len) do
          {left, right} = String.split_at(str, len)
      
          if String.length(right) < len do
            [left]
          else
            [left | split_multiple(right, len)]
          end
        end
      
        def parse_range(id_range) do
          [left, right] = String.split(id_range, "-")
          String.to_integer(left)..String.to_integer(right)
        end
      
        def get_invalid_ids(ids, is_invalid_id) do
          ids
          |> String.splitter([","], [:trim])
          |> Task.async_stream(fn range ->
            Enum.filter(parse_range(range), is_invalid_id)
          end)
          |> Enum.flat_map(fn result ->
            {_status, datalist} = result
            datalist
          end)
        end
      
        def sum_invalid_ids(ids, is_invalid_id) do
          ids
          |> get_invalid_ids(is_invalid_id)
          |> Enum.reduce(&(&1 + &2))
        end
      
        def solve_part_one do
          sum_invalid_ids(AocElixir.read_input(2), &is_invalid_id_part_one/1)
        end
      
        def solve_part_two do
          sum_invalid_ids(AocElixir.read_input(2), &is_invalid_id_part_two/1)
        end
      
        def main do
          IO.puts(~s"Part One: #{solve_part_one()}")
          IO.puts(~s"Part Two: #{solve_part_two()}")
        end
      end
      
      2 votes
  8. Berdes
    (edited )
    Link
    The problem today felt about as simple as yesterday, with my logic to find invalid IDs even uglier than the stuff I wrote yesterday. At least, I'm getting a hang of the syntax of Rust and didn't...

    The problem today felt about as simple as yesterday, with my logic to find invalid IDs even uglier than the stuff I wrote yesterday. At least, I'm getting a hang of the syntax of Rust and didn't need to spend much time searching the documentation.

    Instead of going through all the numbers in each range, I'm sure it's feasible to iterate over the first half of each number and directly check if the resulting repeating ID is inside the range. Thinking about it, I'm feeling like this could even yield a simpler logic than what I currently have.

    Part 1 and 2 (Rust)
    use std::io;
    use std::str::FromStr;
    
    struct Range {
      a: u64,
      b: u64,
    }
    
    struct ParseRangeError {
      message: String,
    }
    
    impl FromStr for Range {
      type Err = ParseRangeError;
    
      fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (a, b) = s.split_once('-').ok_or(ParseRangeError {
          message: format!("Invalid range {}, missing -", s),
        })?;
        let ia = a.parse::<u64>().map_err(|e| ParseRangeError {
          message: format!("Invalid number {}: {}", s, e),
        })?;
        let ib = b.parse::<u64>().map_err(|e| ParseRangeError {
          message: format!("Inavlid number {}: {}", s, e),
        })?;
        Ok(Range { a: ia, b: ib })
      }
    }
    
    fn is_invalid1(a: u64) -> bool {
      let mut mul = 10;
      while a > mul {
        if a < mul * mul && a > mul * mul / 10 {
          let tmp = a % mul;
          return a == tmp + tmp * mul;
        }
        mul *= 10;
      }
      return false;
    }
    
    fn is_invalid2(a: u64) -> bool {
      let mut mul = 10;
      while a > mul {
        let p = a % mul;
        let mut pmul = mul;
        while a > pmul {
          if (a / pmul) % mul != p {
            break;
          } else if a < pmul * mul && a > pmul * mul / 10 {
            return true;
          }
          pmul *= mul;
        }
        mul *= 10;
      }
      return false;
    }
    
    fn main() {
      let mut part1 = 0;
      let mut part2 = 0;
      let mut buffer = String::new();
      io::stdin().read_line(&mut buffer).unwrap();
      for range in buffer.trim().split(',').map(|e| e.parse::<Range>()) {
        match range {
          Ok(r) => {
            for n in r.a..r.b + 1 {
              part1 += if is_invalid1(n) { n } else { 0 };
              part2 += if is_invalid2(n) { n } else { 0 };
            }
          }
          Err(e) => panic!("Error reading range: {}", e.message),
        }
      }
      println!("Part 1: {}", part1);
      println!("Part 2: {}", part2);
    }
    

    Edit: Indeed, it's possible to keep track of the part that is going to be repeated and iterate directly over it. Based on the result, it's not exactly simpler, especially since the second part still needs the is_invalid logic to avoid double-counting numbers in the range. At least, it's mighty fast: it's running in ~1ms including all the overhead of starting the process and parsing the input while the previous version took ~120ms.

    Implementation of the sum of invalid IDs over a range for both parts
    fn length(mut a: u64) -> u64 {
      if a == 0 {
        return 1;
      }
      let mut l = 0;
      while a > 0 {
        a /= 10;
        l += 1;
      }
      return l;
    }
    
    fn pow(n: u64, e: u64) -> u64 {
      let mut ret = 1;
      for _ in 0..e {
        ret *= n;
      }
      return ret;
    }
    
    fn sum_range1(r: &Range) -> u64 {
      let l = length(r.a);
      let mut first_half = if l % 2 == 0 {
        r.a / pow(10, l / 2)
      } else {
        pow(10, l / 2)
      };
      let mut ret = 0;
      loop {
        let n = first_half * pow(10, length(first_half)) + first_half;
        if n > r.b {
          break;
        }
        if n >= r.a {
          ret += n;
        }
        first_half += 1;
      }
      return ret;
    }
    
    fn is_invalid2(a: u64) -> bool {
      let mut mul = 10;
      while a > mul {
        let p = a % mul;
        let mut pmul = mul;
        while a > pmul {
          if (a / pmul) % mul != p {
            break;
          } else if a < pmul * mul && a > pmul * mul / 10 {
            return true;
          }
          pmul *= mul;
        }
        mul *= 10;
      }
      return false;
    }
    
    fn sum_range2(r: &Range) -> u64 {
      let la = length(r.a);
      let lb = length(r.b);
      let mut ret = 0;
      for num_parts in 2..lb + 1 {
        let mut part = if la % num_parts == 0 {
          r.a / pow(10, (num_parts - 1) * la / num_parts)
        } else {
          pow(10, la / num_parts)
        };
        loop {
          let mut n = part;
          let mult = pow(10, length(part));
          for _ in 1..num_parts {
            n = n * mult + part;
          }
          if n > r.b {
            break;
          }
          // If the part is invalid, the number formed risks being counted multiple
          // times. E.g. 1111 can be decomposed as four 1 or two 11. When this
          // happens, exactly one decomposition will have a part that is also a
          // valid ID.
          if n >= r.a && !is_invalid2(part) {
            ret += n;
          }
          part += 1;
        }
      }
      return ret;
    }
    

    Edit 2: actually, things can be pushed even further since the sum of invalid IDs (for part 1) in the range 123000..456999 is the same as the sum of numbers 123..456 times 101, which can be computer is O(1) using the fact that the sum of numbers from 1 to n (included) is n*(n+1)/2. Part 2 becomes even more interesting since you also want to exclude all the numbers that are invalid IDs from the sum of the consecutive numbers, which can be done using a recursive call to the function that computes the sum of invalid IDs for a given range. Thus, it's not actually necessary to have an is_valid function for part 2 either.

    1 vote
  9. HunterRobot
    Link
    Trying to get a little bit of Kotlin experience in. Nice little indentation pyramid of doom in part 2, not exactly proud but it works. Part 1 data class Range(val start: Long, val end: Long) fun...

    Trying to get a little bit of Kotlin experience in. Nice little indentation pyramid of doom in part 2, not exactly proud but it works.

    Part 1
    data class Range(val start: Long, val end: Long)
    
    fun main() {
        val input = Files.readString(Path("src/main/resources/day2/input"))
    
        val ranges = input.split(',')
                .map { it.split('-') }
                .map { Range(it[0].toLong(), it[1].toLong()) }
        var invalidIdSum = 0L
    
        ranges.forEach { range ->
            for (id in range.start..range.end) {
                val number = id.toString()
                val midpoint = number.length / 2
                if (number.length % 2 == 0 && number.take(midpoint).toInt() == number.substring(midpoint).toInt()) {
                    invalidIdSum += id
                }
            }
        }
        println("Sum: $invalidIdSum")
    }
    
    Part 2
    data class Range(val start: Long, val end: Long)
    
    fun main() {
        val input = Files.readString(Path("src/main/resources/day2/input"))
    
        val ranges = input.split(',')
                .map { it.split('-') }
                .map { Range(it[0].toLong(), it[1].toLong()) }
        var invalidIdSum = 0L
    
        ranges.forEach { range ->
            rangeLoop@ for (id in range.start..range.end) {
                val idString = id.toString()
                for (patternSize in 1 .. idString.length / 2) {
                    if (idString.length % patternSize == 0) {
                        val chunked = idString.chunked(patternSize)
                        if (chunked.all { it == chunked[0] }) {
                            println("Found invalid id $id for pattern size $patternSize")
                            invalidIdSum += id
                            continue@rangeLoop
                        }
                    }
                }
            }
        }
        println("Sum: $invalidIdSum")
    }
    
    1 vote
  10. balooga
    (edited )
    Link
    TypeScript I posted earlier about my misunderstanding that repeated digits could be repeated more than twice. So I came up with a decent Part 1 solution that "chops" potential IDs within the range...

    TypeScript

    I posted earlier about my misunderstanding that repeated digits could be repeated more than twice. So I came up with a decent Part 1 solution that "chops" potential IDs within the range in half (as strings), repeats the chopped value, then casts back to a number to validate.

    Of course (as I predicted) Part 2 did allow for repeated sequences of arbitrary length, obsoleting my first approach. I ended up doing the following:

    1. Enumerate the possible lengths (numbers of digits) of IDs within the range
    2. Find the proper divisors of each of those, to determine valid lengths of repeating portions for a given ID length
    3. Deduplicate that list to remove any overlaps that were found, and loop through it...
    4. For each possible repeating portion length, loop through the possible ID lengths from step 1
    5. Within that loop, validate that the current repeating portion length is a factor of the current ID length
    6. If so, set up a loop through ALL numbers with the number of digits corresponding to the current repeating portion length.
    7. For each of those, repeat the number however many times are needed to reach the current full ID length
    8. If that resulting ID is within the expected range, mark it as one of the invalid ones
    Parts 1 and 2
    type InputData = [number, number][];
    
    function formatInput(input: string): InputData {
      const inputArr = input.trim().split(',');
      const stringRanges = inputArr.map(range => range.split('-'));
      return stringRanges.map(stringRange => stringRange.map(limitString => parseInt(limitString, 10))) as InputData;
    }
    
    export function run(input: string): string[] {
      const data = formatInput(input);
    
      const totalTwiceRepeatedIds = (ranges: InputData): number => {
        const chopIds = (start: number, end: number): [number, number] => {
          const startString = `${start}`;
          const endString = `${end}`;
          const chopAmount = -1 * (startString.length % 2 === 0 ? startString.length / 2 : Math.ceil(endString.length / 2));
          const choppedStart = parseInt(startString.slice(0, chopAmount), 10) || 0;
          const choppedEnd = parseInt(endString.slice(0, chopAmount), 10) || 0;
          return [choppedStart, choppedEnd];
        };
    
        const invalidIds: number[] = [];
    
        for (const [start, end] of ranges) {
          const [choppedStart, choppedEnd] = chopIds(start, end);
          for (let i = choppedStart; i <= choppedEnd; i++) {
            const testId = parseInt(`${i}${i}`, 10);
            if (testId >= start) {
              if (testId > end) {
                break;
              }
              invalidIds.push(testId);
            }
          }
        }
        return invalidIds.reduce((prev, curr) => curr + prev, 0);
      };
    
      const totalMultiRepeatedIds = (ranges: InputData): number => {
        const getProperDivisors = (dividend: number): number[] => {
          const result: Set<number> = new Set();
          for (let i = 1; i * i <= dividend; i++) {
            if (dividend % i === 0) {
              result.add(i);
              result.add(dividend / i);
            }
          }
          result.delete(dividend);
          return [...result];
        };
    
        const invalidIds: Set<number> = new Set();
    
        for (const [start, end] of ranges) {
          const startString = `${start}`;
          const endString = `${end}`;
    
          const rangeLengths = Array.from(
            { length: endString.length - startString.length + 1 },
            (_, i) => startString.length + i
          );
          const possibleRepetitionLengths = [...new Set(rangeLengths.flatMap(getProperDivisors))].sort((a, b) => a - b);
    
          for (const repetitionLength of possibleRepetitionLengths) {
            for (const rangeLength of rangeLengths) {
              if (rangeLength % repetitionLength === 0) {
                const repetitionCount = rangeLength / repetitionLength;
                for (let i = 10 ** (repetitionLength - 1); `${i}`.length === repetitionLength; i++) {
                  const testId = parseInt(`${i}`.repeat(repetitionCount), 10);
                  if (testId > 9 && testId >= start) {
                    if (testId > end) {
                      break;
                    }
                    invalidIds.add(testId);
                  }
                }
              }
            }
          }
        }
        return [...invalidIds].reduce((prev, curr) => curr + prev, 0);
      };
    
      return [`${totalTwiceRepeatedIds(data)}`, `${totalMultiRepeatedIds(data)}`];
    }
    

    Edited to add benchmarks:
    Part 1 completes in 1ms on my machine; Part 2 completes in 19ms.

    1 vote
  11. mawelborn
    (edited )
    Link
    More Python generator madness on Day 02! Super interesting to see how everyone approached the "has repetition" check. For my part, I used an all() substring comparison approach for any() valid...

    More Python generator madness on Day 02! Super interesting to see how everyone approached the "has repetition" check. For my part, I used an all() substring comparison approach for any() valid substring length.

    Part 1
    def solve(input: str) -> int:
        return sum(filter(twiced, product_ids(input)))
    
    
    def twiced(product_id: int) -> bool:
        product_id_str = str(product_id)
        product_id_len = len(product_id_str)
        return (
            product_id_len % 2 == 0
            and product_id_str[: product_id_len // 2]
            == product_id_str[product_id_len // 2 :]
        )
    
    
    def product_ids(input: str) -> Iterator[int]:
        for start_end in input.split(","):
            start, end = start_end.split("-")
            for product_id in range(int(start), int(end) + 1, 1):
                yield product_id
    
    Part 2
    def solve(input: str) -> int:
        return sum(filter(repetition, product_ids(input)))
    
    
    def repetition(product_id: int) -> bool:
        product_id_str = str(product_id)
        product_id_len = len(product_id_str)
        return any(
            repetition_of(repetition_len, product_id_str, product_id_len)
            for repetition_len in repetition_lens(product_id_len)
        )
    
    
    def repetition_of(rep_len: int, product_id_str: str, product_id_len: int) -> bool:
        return all(
            product_id_str[:rep_len]
            == product_id_str[rep_len * rep_mult : rep_len * (rep_mult + 1)]
            for rep_mult in range(1, product_id_len // rep_len, 1)
        )
    
    
    def repetition_lens(product_id_len: int) -> Iterator[int]:
        for repetition_len in range(product_id_len // 2):
            repetition_len += 1
            if product_id_len % repetition_len == 0:
                yield repetition_len
    
    
    def product_ids(input: str) -> Iterator[int]:
        for start_end in input.split(","):
            start, end = start_end.split("-")
            for product_id in range(int(start), int(end) + 1, 1):
                yield product_id
    
    1 vote
  12. creamsnail
    Link
    Part two really was painful for some reason. I couldn't wrap my mind around how to find all the duplicates when it could just be two, or three or more. So this is what I ended up with. I feel like...

    Part two really was painful for some reason. I couldn't wrap my mind around how to find all the duplicates when it could just be two, or three or more. So this is what I ended up with. I feel like I was going down a brute force path there for a bit before coming to this solution.

    Rust - Parts 1 and 2
    use std::fs::read_to_string;
    
    const TEST_FILE: &str = "example_input.txt";
    const PUZZLE_INPUT: &str = "puzzle_input.txt";
    
    fn main() {
        let part_one_answer = part_one(read_puzzle_input(PUZZLE_INPUT));
        println!("The answer for part one is: {}", part_one_answer);
    
        let part_two_answer = part_two(read_puzzle_input(PUZZLE_INPUT));
        println!("The answer for part two is: {}", part_two_answer);
    }
    
    // Reads in the puzzle input text file which contains the D##
    fn read_puzzle_input(file_name: &str) -> Vec<String> {
        return read_to_string(file_name)
            .unwrap()
            .lines()
            .map(String::from)
            .collect();
    }
    
    fn part_one(id_ranges: Vec<String>) -> i64 {
        // For us to add all the invalid numbers
        let mut invalid_id_total: i64 = 0;
        // Iterate through the ranges provided
        for line in id_ranges {
            // Split the string into two because for some reason this is oddly complicated to accomplish in rust?
            if let Some((start_range, end_range)) = line.split_once('-') {
                // Set our strings to real numbers to build our for loop range
                let start: i64 = start_range.parse::<i64>().unwrap();
                let end: i64 = end_range.parse::<i64>().unwrap();
                // Iterate over our range
                for number in start..end + 1 {
                    // Re-convert the number to a string
                    let str_number = number.to_string();
                    // Split the string into two bits and compare
                    let (first_half, second_half) = str_number.split_at(str_number.len() / 2);
                    if first_half == second_half {
                        //println!("Found invalid ID: {}", str_number);
                        invalid_id_total += number
                    }
                }
            } else {
                // Would normally handle an error here due to the Some() above in case the `-` didn't exist.
                continue;
            }
        }
        return invalid_id_total;
    }
    
    fn part_two(id_ranges: Vec<String>) -> i64 {
        // For us to add all the invalid numbers
        let mut invalid_id_total: i64 = 0;
        // Iterate through the ranges provided
        for line in id_ranges {
            // Split the string into two because for some reason this is oddly complicated to accomplish in rust?
            if let Some((start_range, end_range)) = line.split_once('-') {
                // Set our strings to real numbers to build our for loop range
                let start: i64 = start_range.parse::<i64>().unwrap();
                let end: i64 = end_range.parse::<i64>().unwrap();
                // Iterate over our range
                for number in start..end + 1 {
                    // Re-convert the number to a string
                    let str_number = number.to_string();
                    // Split the string into two bits and compare
                    let (first_half, second_half) = str_number.split_at(str_number.len() / 2);
                    let mut first_split: Vec<&str> = str_number.split(&str_number[..1]).collect();
                    first_split.dedup();
                    if first_half == second_half {
                        //println!("Found invalid ID: {}", str_number);
                        invalid_id_total += number;
                        println!("Invalid Number: {:?}", number);
                    } else if number > 9 && first_split.len() == 1 {
                        invalid_id_total += number;
                        println!("Invalid Number: {:?}", number);
                    } else {
                        // Iterate through the first few to see if there are repeated numbers like 2,3,4 characters in repeated
                        for i in 1..str_number.len() / 2 {
                            let mut test_vec: Vec<&str> = str_number.split(&str_number[..i]).collect();
                            test_vec.dedup();
                            if test_vec.len() == 1 {
                                invalid_id_total += number;
                                println!("Invalid Number: {:?}", number);
                            }
                        }
                    }
                }
            } else {
                // Would normally handle an error here due to the Some() above in case the `-` didn't exist.
                continue;
            }
        }
        return invalid_id_total;
    }
    
    #[test]
    fn part_one_example() {
        let instructions = read_puzzle_input(TEST_FILE);
        let added_invalid_ids = part_one(instructions);
        assert_eq!(added_invalid_ids, 1227775554);
    }
    
    #[test]
    fn part_two_example() {
        let instructions = read_puzzle_input(TEST_FILE);
        let added_invalid_ids = part_two(instructions);
        assert_eq!(added_invalid_ids, 4174379265);
    }
    
    1 vote
  13. tjf
    Link
    For now I've bruteforced both parts of my Python solution. When I'm more awake maybe I'll revisit it and try to be more clever. But simply using PyPy instead of CPython cuts my part 2 runtime from...

    For now I've bruteforced both parts of my Python solution. When I'm more awake maybe I'll revisit it and try to be more clever. But simply using PyPy instead of CPython cuts my part 2 runtime from 20s to 1.5s. Good enough for now.

    Part 1
    #!/usr/bin/env python3
    
    import sys
    
    total = 0
    for interval in sys.stdin.read().strip().split(','):
        lower, upper = map(int, interval.split('-'))
        for i in range(lower, upper + 1):
            s = str(i)
            mid = len(s) // 2
            if s[:mid] == s[mid:]:
                total += i
    
    print(total)
    
    Part 2
    #!/usr/bin/env python3
    
    import sys
    
    total = 0
    for interval in sys.stdin.read().strip().split(','):
        lower, upper = map(int, interval.split('-'))
        for i in range(lower, upper + 1):
            s = str(i)
            for nrepeats in range(2, len(s) + 1):
                if len(s) % nrepeats:
                    continue
                repeatlen = len(s) // nrepeats
                repeats = True
                for repeatstart in range(0, len(s) - repeatlen, repeatlen):
                    a = s[repeatstart:repeatstart + repeatlen]
                    b = s[repeatstart + repeatlen:repeatstart + 2 * repeatlen]
                    if a != b:
                        repeats = False
                if repeats:
                    total += i
                    break
    
    print(total)
    
    1 vote
  14. DeaconBlue
    Link
    I am pretty happy with my solution on this one. Early breaking when I see a failure gets this down to ~300ms on my laptop. I am absolutely sure the same algorithm could be done in a better way if...

    I am pretty happy with my solution on this one. Early breaking when I see a failure gets this down to ~300ms on my laptop. I am absolutely sure the same algorithm could be done in a better way if I knew Rust better.

    I only have the Part 2 solution because I changed the function call to do the whole thing, not thinking about holding both parts.

    Part 2
    use std::{
        env,
        fs::File,
        i32,
        io::{self, BufRead},
        path::Path,
        time::Instant,
    };
    
    fn main() {
        let now = Instant::now();
        let args: Vec<String> = env::args().collect();
        let mut total_invalid: i64 = 0;
        if let Ok(lines) = read_lines(&args[1]) {
            for line in lines {
                line.expect("")
                    .split(',')
                    .for_each(|x| total_invalid += add_range(x));
            }
        }
        println!("Total invalid: {total_invalid}");
        let elapsed = now.elapsed();
        println!("Elapsed: {:.2?}", elapsed);
    }
    fn add_range(range: &str) -> i64 {
        println!("{range}");
        let range = get_bounds(range);
        let mut temp_total = 0;
        for n in range.0..=range.1 {
            if is_invalid(n) {
                temp_total = temp_total + n;
            }
        }
        return temp_total;
    }
    
    fn get_bounds(bounds: &str) -> (i64, i64) {
        let mut split = bounds.split("-");
        let lower = split
            .next()
            .expect("")
            .parse::<i64>()
            .expect("Parsing Error");
        let higher = split
            .next()
            .expect("")
            .parse::<i64>()
            .expect("Parsing Error");
    
        return (lower, higher);
    }
    fn is_invalid(id: i64) -> bool {
        let id = id.to_string();
        let id_length = id.len();
        for n in 1..=id_length / 2 {
            let mut is_matching = id_length % n == 0;
            let split = id.split_at(n);
            let mut to_check = split.1;
            let mut index = 0;
            let max_index = to_check.len() / n;
            while is_matching && index < max_index {
                let chunks = to_check.split_at(n);
                is_matching = split.0 == chunks.0;
                to_check = chunks.1;
                index += 1;
            }
            if is_matching {
                println!("{id}");
                return true;
            }
        }
        return false;
    }
    
    fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
    where
        P: AsRef<Path>,
    {
        let file = File::open(filename)?;
        Ok(io::BufReader::new(file).lines())
    }
    
    1 vote
  15. zkxs
    Link
    Rust Part 1 - This was a weird puzzle for me: I don't do a lot of generating strings that match a pattern—usually it's the opposite, where I check if a string matches a pattern. For performance,...

    Rust

    Part 1 - This was a weird puzzle for me: I don't do a lot of generating strings that match a pattern—usually it's the opposite, where I check if a string matches a pattern. For performance, generating is definitely the way to go here vs brute-force scanning all the ranges for invalid IDs. After a lot of fiddling I was able to get an invalid ID generator working, and it's pretty fast!

    Part 2 - I took one look at part 2 and decided I didn't want to re-do all that fiddling to make my solution more generic. 😭 This is just naive brute-forcing and it's many orders of magnitude slower than it ought to be.

    1 vote
  16. [4]
    balooga
    Link
    I haven’t started writing code yet, just looking at the puzzle for now. Something’s bugging me… Can someone help me understand why 111 and 999 are apparently NOT invalid? I thought I understood...

    I haven’t started writing code yet, just looking at the puzzle for now. Something’s bugging me…

    Since the young Elf was just doing silly patterns, you can find the invalid IDs by looking for any ID which is made only of some sequence of digits repeated twice. So, 55 (5 twice), 6464 (64 twice), and 123123 (123 twice) would all be invalid IDs.

    None of the numbers have leading zeroes; 0101 isn't an ID at all. (101 is a valid ID that you would ignore.)

    Your job is to find all of the invalid IDs that appear in the given ranges. In the above example:

    • 11-22 has two invalid IDs, 11 and 22.
    • 95-115 has one invalid ID, 99.
    • 998-1012 has one invalid ID, 1010.
      […]

    Can someone help me understand why 111 and 999 are apparently NOT invalid? I thought I understood the rule but those two are throwing me off.

    1. [3]
      jonah
      Link Parent
      Because in those numbers, the repeated digits are repeated more than twice

      Because in those numbers, the repeated digits are repeated more than twice

      2 votes
      1. [2]
        balooga
        Link Parent
        D’oh! Thanks! That should actually make the logic a good deal simpler (at least until the Part 2 complication comes along and undoes everything…)!

        D’oh! Thanks!

        That should actually make the logic a good deal simpler (at least until the Part 2 complication comes along and undoes everything…)!

        2 votes
        1. jonah
          Link Parent
          You're welcome! good luck and have fun

          You're welcome! good luck and have fun

          1 vote