Skip Navigation

🌉 - 2024 DAY 7 SOLUTIONS - 🌉

Day 7: Bridge Repair

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

53 comments
  • Uiua

    This turned out to be reasonably easy in Uiua, though this solution relies on macros which maybe slow it down.

    (edit: removing one macro sped it up quite a bit)

    (edit2: Letting Uiua build up an n-dimensional array turned out to be the solution, though sadly my mind only works in 3 dimensions. Now runs against the live data in around 0.3 seconds.)

    Try it here

     
        
    Data   ← ⊜(□⊜⋕⊸(¬∈": "))⊸≠@\n "190: 10 19\n3267: 81 40 27\n83: 17 5\n156: 15 6\n7290: 6 8 6 15\n161011: 16 10 13\n192: 17 8 14\n21037: 9 7 18 13\n292: 11 6 16 20"
    Calib! ← ≡◇⊢▽⊸≡◇(∈♭/[^0]:°⊂) # Calibration targets which can be constructed from their values.
    &p/+Calib!⊃(+|×)Data
    &p/+Calib!⊃(+|×|+×ⁿ:10+1⌊ₙ₁₀,)Data
    
      
  • Python

    It is a tree search

     
        
    def parse_input(path):
    
      with path.open("r") as fp:
        lines = fp.read().splitlines()
    
      roots = [int(line.split(':')[0]) for line in lines]
      node_lists = [[int(x)  for x in line.split(':')[1][1:].split(' ')] for line in lines]
    
      return roots, node_lists
    
    def construct_tree(root, nodes, include_concat):
    
      levels = [[] for _ in range(len(nodes)+1)]
      levels[0] = [(str(root), "")]
      # level nodes are tuples of the form (val, operation) where both are str
      # val can be numerical or empty string
      # operation can be *, +, || or empty string
    
      for indl, level in enumerate(levels[1:], start=1):
    
        node = nodes[indl-1]
    
        for elem in levels[indl-1]:
    
          if elem[0]=='':
            continue
    
          if elem[0][-len(str(node)):] == str(node) and include_concat:
            levels[indl].append((elem[0][:-len(str(node))], "||"))
          if (a:=int(elem[0]))%(b:=int(node))==0:
            levels[indl].append((str(int(a/b)), '*'))
          if (a:=int(elem[0])) - (b:=int(node))>0:
            levels[indl].append((str(a - b), "+"))
    
      return levels[-1]
    
    def solve_problem(file_name, include_concat):
    
      roots, node_lists = parse_input(Path(cwd, file_name))
      valid_roots = []
    
      for root, nodes in zip(roots, node_lists):
    
        top = construct_tree(root, nodes[::-1], include_concat)
    
        if any((x[0]=='1' and x[1]=='*') or (x[0]=='0' and x[1]=='+') or
               (x[0]=='' and x[1]=='||') for x in top):
    
          valid_roots.append(root)
    
      return sum(valid_roots)
    
      
    • I asked ChatGPT to explain your code and mentioned you said it was a binary search. idk why, but it output a matter of fact response that claims you were wrong. lmao, I still don't understand how your code works

      This code doesn’t perform a classic binary search. Instead, it uses each input node to generate new possible states or “branches,” forming a tree of transformations. At each level, it tries up to three operations on the current value (remove digits, divide, subtract). These expansions create multiple paths, and the code checks which paths end in a successful condition. While the author may have described it as a “binary search,” it’s more accurately a state-space search over a tree of possible outcomes, not a binary search over a sorted data structure.

      I understand it now! I took your solution and made it faster. it is now like 36 milliseconds faster for me. which is interesting because if you look at the code. I dont process the entire list of integers. I sometimes stop prematurely before the next level, clear it, and add the root. I don't know why but it just works for my input and the test input.

      however what I notice is that the parse_input causes it to be the reason why it is slower by 20+ milliseconds. I find that even if I edited your solution like so to be slightly faster, it is still 10 milliseconds slower than mine: :::spoiler code

       py
          
      def parse_input():
      
        with open('input',"r") as fp:
          lines = fp.read().splitlines()
      
        roots = [int(line.split(':')[0]) for line in lines]
        node_lists = [[int(x) for x in line.split(': ')[1].split(' ')] for line in lines]
      
        return roots, node_lists
      
      def construct_tree(root, nodes):
          levels = [[] for _ in range(len(nodes)+1)]
          levels[0] = [(root, "")]
          # level nodes are tuples of the form (val, operation) where both are str
          # val can be numerical or empty string
          # operation can be *, +, || or empty string
      
          for indl, level in enumerate(levels[1:], start=1):
      
              node = nodes[indl-1]
      
              for elem in levels[indl-1]:
                  if elem[0]=='':
                      continue
      
                  if (a:=elem[0])%(b:=node)==0:
                      levels[indl].append((a/b, '*'))
                  if (a:=elem[0]) - (b:=node)>0:
                      levels[indl].append((a - b, "+"))
      
          return levels[-1]
      
      
      def construct_tree_concat(root, nodes):
          levels = [[] for _ in range(len(nodes)+1)]
          levels[0] = [(str(root), "")]
          # level nodes are tuples of the form (val, operation) where both are str
          # val can be numerical or empty string
          # operation can be *, +, || or empty string
      
          for indl, level in enumerate(levels[1:], start=1):
      
              node = nodes[indl-1]
      
              for elem in levels[indl-1]:
                  if elem[0]=='':
                      continue
      
                  if elem[0][-len(str(node)):] == str(node):
                      levels[indl].append((elem[0][:-len(str(node))], "||"))
                  if (a:=int(elem[0]))%(b:=int(node))==0:
                      levels[indl].append((str(int(a/b)), '*'))
                  if (a:=int(elem[0])) - (b:=int(node))>0:
                      levels[indl].append((str(a - b), "+"))
      
          return levels[-1]
      
      def solve_problem():
      
        roots, node_lists = parse_input()
        valid_roots_part1 = []
        valid_roots_part2 = []
      
        for root, nodes in zip(roots, node_lists):
          
          top = construct_tree(root, nodes[::-1])
      
          if any((x[0]==1 and x[1]=='*') or (x[0]==0 and x[1]=='+') for x in top):
            valid_roots_part1.append(root)
            
          top = construct_tree_concat(root, nodes[::-1])
      
          if any((x[0]=='1' and x[1]=='*') or (x[0]=='0' and x[1]=='+') or (x[0]=='' and x[1]=='||') for x in top):
      
            valid_roots_part2.append(root)
      
        return sum(valid_roots_part1),sum(valid_roots_part2)
        
      if __name__ == "__main__":
          print(solve_problem())
      
      
        

      :::

      • Wow I got thrashed by chatgpt. Strictly speaking that is correct, it is more akin to Tree Search. But even then not strictly because in tree search you are searching through a list of objects that is known, you build a tree out of it and based on some conditions eliminate half of the remaining tree each time. Here I have some state space (as chatgpt claims!) and again based on applying certain conditions, I eliminate some portion of the search space successively (so I dont have to evaluate that part of the tree anymore). To me both are very similar in spirit as both methods avoid evaluating some function on all the possible inputs and successively chops off a fraction of the search space. To be more correct I will atleast replace it with tree search though, thanks. And thanks for taking a close look at my solution and improving it.

  • Nim

    Bruteforce, my beloved.

    Wasted too much time on part 2 trying to make combinations iterator (it was very slow). In the end solved both parts with 3^n and toTernary.

    Runtime: 1.5s

     nim
        
    func digits(n: int): int =
      result = 1; var n = n
      while (n = n div 10; n) > 0: inc result
    
    func concat(a: var int, b: int) =
      a = a * (10 ^ b.digits) + b
    
    func toTernary(n: int, len: int): seq[int] =
      result = newSeq[int](len)
      if n == 0: return
      var n = n
      for i in 0..<len:
        result[i] = n mod 3
        n = n div 3
    
    proc solve(input: string): AOCSolution[int, int] =
      for line in input.splitLines():
        let parts = line.split({':',' '})
        let res = parts[0].parseInt
        var values: seq[int]
        for i in 2..parts.high:
          values.add parts[i].parseInt
    
        let opsCount = values.len - 1
        var solvable = (p1: false, p2: false)
        for s in 0 ..< 3^opsCount:
          var sum = values[0]
          let ternary = s.toTernary(opsCount)
          for i, c in ternary:
            case c
            of 0: sum *= values[i+1]
            of 1: sum += values[i+1]
            of 2: sum.concat values[i+1]
            else: raiseAssert"!!"
          if sum == res:
            if ternary.count(2) == 0:
              solvable.p1 = true
            solvable.p2 = true
            if solvable == (true, true): break
        if solvable.p1: result.part1 += res
        if solvable.p2: result.part2 += res
    
      
  • Java

    Today was pretty easy one but for some reason I spent like 20 minutes overthinking part 2 when all it needed was one new else if. I initially through the concatenation operator would take precedence even tho it clearly says "All operators are still evaluated left-to-right" in the instructions..

    I'm sure there are optimizations to do but using parallelStreams it only takes around 300ms total on my machine so there's no point really

  • Dart

    Suspiciously easy, so let's see how tomorrow goes.... (edit: forgot to put the language! Dart for now, thinking about Uiua later)

     
        
    import 'package:more/more.dart';
    
    var ops = [(a, b) => a + b, (a, b) => a * b, (a, b) => int.parse('$a$b')];
    
    bool canMake(int target, List<int> ns, int sofar, dynamic ops) {
      if (ns.isEmpty) return target == sofar;
      for (var op in ops) {
        if (canMake(target, ns.sublist(1), op(sofar, ns.first), ops)) return true;
      }
      return false;
    }
    
    solve(List<String> lines, dynamic ops) {
      var sum = 0;
      for (var line in lines.map((e) => e.split(' '))) {
        var target = int.parse(line.first.skipLast(1));
        var ns = line.skip(1).map(int.parse).toList();
        sum += (canMake(target, ns.sublist(1), ns.first, ops)) ? target : 0;
      }
      return sum;
    }
    
    part1(List<String> lines) => solve(lines, ops.sublist(0, 2));
    part2(List<String> lines) => solve(lines, ops);
    
      
  • J

    Didn't try to make it clever at all, so it's fairly slow (minutes, not seconds). Maybe rewriting foldl_ops in terms of destructive array update would improve matters, but the biggest problem is that I don't skip unnecessary calculations (because we've already found a match or already reached too big a number). This is concise and follows clearly from the definitions, however.

     
        
    data_file_name =: '7.data
    lines =: cutopen fread data_file_name
    NB. parse_line yields a boxed vector of length 2, target ; operands
    NB. &amp;. is "under": u &amp;. v is v^:_1 @: u @: v with right rank of v
    parse_line =: monad : '(". &amp;. >) (>y) ({.~ ; (}.~ >:)) '':'' i.~ >y'
    NB. m foldl_ops n left folds n by the string of binary operators named by m,
    NB. as indices into the global operators, the leftmost element of m naming
    NB. an operator between the leftmost two elements of n. #m must be #n - 1.
    foldl_ops =: dyad define
       if. 1 >: # y do. {. y else.
          (}. x) foldl_ops (((operators @. ({. x))/ 2 {. y) , 2 }. y)
       end.
    )
    NB. b digit_strings n enumerates i.b^n as right justified digit strings
    digit_strings =: dyad : '(y # x) #:"1 0 i. x ^ y'
    feasible =: dyad define
       operators =: x  NB. global
       'target operands' =. y
       +./ target = ((# operators) digit_strings (&lt;: # operands)) foldl_ops"1 operands
    )
    compute =: monad : '+/ ((> @: {.) * (y &amp; feasible))"1 parse_line"0 lines'
    result1 =: compute +`*
    
    concat =: , &amp;.: (10 &amp; #.^:_1)
    result2 =: compute +`*`concat
    
    
      
  • C#

     
        
    public class Day07 : Solver
    {
      private ImmutableList<(long, ImmutableList<long>)> equations;
    
      public void Presolve(string input) {
        equations = input.Trim().Split("\n")
          .Select(line => line.Split(": "))
          .Select(split => (long.Parse(split[0]), split[1].Split(" ").Select(long.Parse).ToImmutableList()))
          .ToImmutableList();
      }
    
      private bool TrySolveWithConcat(long lhs, long head, ImmutableList<long> tail) {
        var lhs_string = lhs.ToString();
        var head_string = head.ToString();
        return lhs_string.Length > head_string.Length &&
          lhs_string.EndsWith(head_string) &&
          SolveEquation(long.Parse(lhs_string.Substring(0, lhs_string.Length - head_string.Length)), tail, true);
      }
    
      private bool SolveEquation(long lhs, ImmutableList<long> rhs, bool with_concat = false) {
        if (rhs.Count == 1) return lhs == rhs[0];
        long head = rhs[rhs.Count - 1];
        var tail = rhs.GetRange(0, rhs.Count - 1);
        return (SolveEquation(lhs - head, tail, with_concat))
          || (lhs % head == 0) && SolveEquation(lhs / head, tail, with_concat)
          || with_concat && TrySolveWithConcat(lhs, head, tail);
      }
    
      public string SolveFirst() => equations
        .Where(eq => SolveEquation(eq.Item1, eq.Item2))
        .Select(eq => eq.Item1)
        .Sum().ToString();
      public string SolveSecond() => equations
        .Where(eq => SolveEquation(eq.Item1, eq.Item2, true))
        .Select(eq => eq.Item1)
        .Sum().ToString();
    }
    
      
  • Made a couple of attempts to munge the input data into some kind of binary search tree, lost some time to that, then threw my hands into the air and did a more naïve sort-of breadth-first search instead. Which turned out to be better for part 2 anyway.
    Also, maths. Runs in just over a hundred milliseconds when using AsParallel, around half a second without.

    ::: spoiler C#

     csharp
        
    List<(long, int[])> data = new List<(long, int[])>();
    
    public void Input(IEnumerable<string> lines)
    {
      foreach (var line in lines)
      {
        var parts = line.Split(':', StringSplitOptions.TrimEntries);
    
        data.Add((long.Parse(parts.First()), parts.Last().Split(' ').Select(int.Parse).ToArray()));
      }
    }
    
    public void Part1()
    {
      var correct = data.Where(kv => CalcPart(kv.Item1, kv.Item2)).Select(kv => kv.Item1).Sum();
    
      Console.WriteLine($"Correct: {correct}");
    }
    public void Part2()
    {
      var correct = data.AsParallel().Where(kv => CalcPart2(kv.Item1, kv.Item2)).Select(kv => kv.Item1).Sum();
    
      Console.WriteLine($"Correct: {correct}");
    }
    
    public bool CalcPart(long res, Span<int> num, long carried = 0)
    {
      var next = num[0];
      if (num.Length == 1)
        return res == carried + next || res == carried * next;
      return CalcPart(res, num.Slice(1), carried + next) || CalcPart(res, num.Slice(1), carried * next);
    }
    
    public bool CalcPart2(long res, Span<int> num, long carried = 0)
    {
      var next = num[0];
      // Get the 10 logarithm for the next number, expand the carried value by 10^<next 10log + 1>, add the two together
      // For 123 || 45
      // 45 ⇒ 10log(45) + 1 == 2
      // 123 * 10^2 + 45 == 12345
      long combined = carried * (long)Math.Pow(10, Math.Floor(Math.Log10(next) + 1)) + next;
      if (num.Length == 1)
        return res == carried + next || res == carried * next || res == combined;
      return CalcPart2(res, num.Slice(1), carried + next) || CalcPart2(res, num.Slice(1), carried * next) || CalcPart2(res, num.Slice(1), combined);
    }
    
      
  • Julia

    Took quite some time to debug but in the end I think it's a nice solution using base 2 and 3 numbers counting up to check all operator combinations.

  • I'm way behind, but I'm trying to learn F#.

    I'm using the library Combinatorics in dotnet, which I've used in the past, generate in this case every duplicating possibility of the operations. I the only optimization that I did was to use a function to concatenate numbers without converting to strings, but that didn't actually help much.

    I have parser helpers that use ReadOnlySpans over strings to prevent unnecessary allocations. However, here I'm adding to a C# mutable list and then converting to an FSharp (linked) list, which this language is more familiar with. Not optimal, but runtime was pretty good.

    I'm not terribly good with F#, but I think I did ok for this challenge.

    F#

     fsharp
        
    // in another file:
    let concatenateLong (a:Int64) (b:Int64) : Int64 =
        let rec countDigits (n:int64) =
            if n = 0 then 0
            else 1 + countDigits (n / (int64 10))   
    
        let bDigits = if b = 0 then 1 else countDigits b
        let multiplier = pown 10 bDigits |> int64
        a * multiplier + b
    
    // challenge file
    type Operation = {Total:Int64; Inputs:Int64 list }
    
    let parse (s:ReadOnlySpan<char>) : Operation =
        let sep = s.IndexOf(':')
        let total = Int64.Parse(s.Slice(0, sep))
        let inputs = System.Collections.Generic.List<Int64>()
        let right:ReadOnlySpan<char> = s.Slice(sep + 1).Trim()
    
       // because the Split function on a span returns a SpanSplitEnumerator, which is a ref-struct and can only live on the stack, 
       // I can't use the F# list syntax here
        for range in right.Split(" ") do
            inputs.Add(Int64.Parse(sliceRange right range))
            
        {Total = total; Inputs = List.ofSeq(inputs) }
    
    let part1Ops = [(+); (*)]
    
    let execute ops input =
        input
        |> PSeq.choose (fun op ->
            let total = op.Total
            let inputs = op.Inputs
            let variations = Variations(ops, inputs.Length - 1, GenerateOption.WithRepetition)
            variations
            |> Seq.tryFind (fun v ->
                let calcTotal = (inputs[0], inputs[1..], List.ofSeq(v)) |||> List.fold2 (fun acc n f -> f acc n) 
                calcTotal = total
                )
            |> Option.map(fun _ -> total)
            )
        |> PSeq.fold (fun acc n -> acc + n) 0L
    
    let part1 input =
        (read input parse)
        |> execute part1Ops
    
    let part2Ops = [(+); (*); concatenateLong]
    
    let part2 input = (read input parse) |> execute part2Ops
    
      

    The Gen0 garbage collection looks absurd, but Gen0 is generally considered "free".

    MethodMeanErrorStdDevGen0Gen1Allocated
    Part119.20 ms0.372 ms0.545 ms17843.7500156.2500106.55 MB
    Part217.94 ms0.355 ms0.878 ms17843.7500156.2500106.55 MB

    V2 - concatenate numbers did little for the runtime, but did help with Gen1 garbage, but not the overall allocation.

    MethodMeanErrorStdDevGen0Gen1Allocated
    Part117.34 ms0.342 ms0.336 ms17843.7500125.0000106.55 MB
    Part217.24 ms0.323 ms0.270 ms17843.750093.7500106.55 MB
53 comments