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

  • iAvicenna@lemmy.world
    link
    fedilink
    arrow-up
    4
    ·
    edit-2
    19 days ago

    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)
    
    • Acters@lemmy.world
      link
      fedilink
      arrow-up
      1
      ·
      edit-2
      19 days ago

      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.

      code
      def main(input_data):
          input_data = input_data.replace('\r', '')
          parsed_data = {int(line[0]): [int(i) for i in line[1].split()[::-1]] for line in [l.split(': ') for l in input_data.splitlines()]}
          part1 = 0
          part2 = 0
          for item in parsed_data.items():
              root, num_array = item
              part_1_branches = [set() for _ in range(len(num_array)+1)]
              part_2_branches = [set() for _ in range(len(num_array)+1)]
              part_1_branches[0].add(root)
              part_2_branches[0].add(root)
              for level,i in enumerate(num_array):
                  if len(part_1_branches[level]) == 0 and len(part_2_branches[level]) == 0:
                      break
      
                  for branch in part_1_branches[level]:
                      if branch == i:
                          part_1_branches[level+1] = set() # clear next level to prevent adding root again
                          part1 += root
                          break
                      if branch % i == 0:
                          part_1_branches[level+1].add(branch//i)
                      if branch - i > 0:
                          part_1_branches[level+1].add(branch-i)
      
                  for branch in part_2_branches[level]:
                      if branch == i or str(branch) == str(i):
                          part_2_branches[level+1] = set() # clear next level to prevent adding root again
                          part2 += root
                          break
                      if branch % i == 0:
                          part_2_branches[level+1].add(branch//i)
                      if branch - i > 0:
                          part_2_branches[level+1].add(branch-i)
                      if str(i) == str(branch)[-len(str(i)):]:
                          part_2_branches[level+1].add(int(str(branch)[:-len(str(i))]))
          print("Part 1:", part1, "\nPart 2:", part2)
          return [part1, part2]
      
      if __name__ == "__main__":
          with open('input', 'r') as f:
              main(f.read())
      

      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:

      code
      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())
      
      • iAvicenna@lemmy.world
        link
        fedilink
        arrow-up
        2
        ·
        edit-2
        19 days ago

        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.

        • Acters@lemmy.world
          link
          fedilink
          arrow-up
          1
          ·
          18 days ago

          idk why my gpt decided to be like that. I expected a long winded response with a little bit of ai hallucinations. I was flabbergasted, and just had to post it.

          I seemingly realized that working forward through the list of integers was inefficient for me to do, and I was using multiprocessing to do it too! which my old solution took less than 15 seconds for my input. your solution to reverse the operations and eliminate paths was brilliant and made it solve it in milliseconds without spinning up my fans, lol

  • mykl@lemmy.world
    link
    fedilink
    arrow-up
    3
    ·
    edit-2
    21 days ago

    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
    
    • Quant@programming.dev
      link
      fedilink
      arrow-up
      1
      ·
      21 days ago

      Thanks to your solution I learned more about how to use reduce :D

      My solution did work for the example input but not for the actual one. When I went here and saw this tiny code block and you saying

      This turned out to be reasonably easy

      I was quite taken aback. And it’s so much better performance-wise too :D (well, until part 2 comes along in my case. Whatever this black magic is you used there is too high for my fried brain atm)

      • mykl@lemmy.world
        link
        fedilink
        arrow-up
        1
        ·
        21 days ago

        Haha, sorry about that, it does seem quite smug :-) I went into it expecting it to be a nightmare of boxes and dimensions, but finding it something I could deal with was a massive relief. Of course once I had a working solution I reversed it back into a multi-dimensional nightmare. That’s where the performance gains came from: about 10x speedup from letting Uiua build up as many dimensions as it needed before doing a final deshaping.

        I enjoyed reading a different approach to this, and thanks for reminding me that ⋕$"__" exists, that’s a great idiom to have up your sleeve.

        Let me know if there’s any bits of my solution that you’d like me to talk you through.

        • Quant@programming.dev
          link
          fedilink
          arrow-up
          1
          ·
          21 days ago

          No worries, it does seem a lot less difficult in hindsight now, my mind just blanked at what I expected to be a lot more code :))

          That performance improvement is amazing, I’ll definitely take a look at how that works in detail later. Just gotta recover from the mental stretch gymnastics trying to remember the state of the stack at different code positions

  • wer2@lemm.ee
    link
    fedilink
    arrow-up
    2
    ·
    20 days ago

    Lisp

    Could probably go much faster if I kept track of calculations to not repeat, but 4 seconds for part 2 on my old laptop is good enough for me. Also, not really a big change from part 1 to part 2.

    Part 1 and 2
    
    (defstruct calibration result inputs)
    
    (defun p1-process-line (line)
      (let ((parts (str:words line)))
        (make-calibration :result (parse-integer (car parts) :junk-allowed t)
                          :inputs (mapcar #'parse-integer (cdr parts)))))
    
    (defun apply-opperators (c opps)
      (let ((accum (car (calibration-inputs c))))
      (loop for o in opps
            for v in (cdr (calibration-inputs c))
            until (> accum (calibration-result c))
            if (eql o 'ADD)
              do (setf accum (+ accum v))
            else if (eql o 'MUL)
              do (setf accum (* accum v))
            else
              do (setf accum (+ v (* accum (expt 10 (1+ (floor (log v 10)))))))
            finally (return accum)
            )))
    
    (defun generate-operators (item-count)
      (labels ((g-rec (c results)
                 (if (< c 1)
                     results
                     (g-rec (1- c) (loop for r in results
                                         collect (cons 'ADD r)
                                         collect (cons 'MUL r))))))
        (g-rec (1- item-count) '((ADD) (MUL)))))
    
    (defun generate-ops-hash (c gen-ops)
      (let ((h (make-hash-table)))
        (dotimes (x c)
          (setf (gethash (+ 2 x) h) (funcall gen-ops (+ 1 x))))
        h))
    
    (defun validate-calibration (c ops-h)
      (let ((r (calibration-result c))
            (ops (gethash (length (calibration-inputs c)) ops-h)))
        (loop for o in ops
              for v = (apply-opperators c o)
              when (= v r)
                return t)))
    
    (defun run-p1 (file) 
      (let ((calibrations  (read-file file #'p1-process-line))
            (ops (generate-ops-hash 13 #'generate-operators)))
        (loop for c in calibrations
              when (validate-calibration c ops)
                sum (calibration-result c))))
    
    (defun generate-operators-p2 (item-count)
      (labels ((g-rec (c results)
                 (if (< c 1)
                     results
                     (g-rec (1- c) (loop for r in results
                                         collect (cons 'ADD r)
                                         collect (cons 'MUL r)
                                         collect (cons 'CAT r))))))
        (g-rec (1- item-count) '((ADD) (MUL) (CAT)))))
    
    (defun run-p2 (file) 
      (let ((calibrations  (read-file file #'p1-process-line))
            (ops (generate-ops-hash 13 #'generate-operators-p2)))
        (loop for c in calibrations
              when (validate-calibration c ops)
                sum (calibration-result c))))
    
    
  • ystael@beehaw.org
    link
    fedilink
    arrow-up
    2
    ·
    edit-2
    20 days ago

    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
    
    
  • Andy@programming.dev
    link
    fedilink
    arrow-up
    2
    ·
    21 days ago

    Factor

    spoiler
    TUPLE: equation value numbers ;
    C: <equation> equation
    
    : get-input ( -- equations )
      "vocab:aoc-2024/07/input.txt" utf8 file-lines [
        split-words unclip but-last string>number
        swap [ string>number ] map <equation>
      ] map ;
    
    : possible-quotations ( funcs numbers -- quots )
      dup length 1 -
      swapd all-selections
      [ unclip swap ] dip
      [ zip concat ] with map
      swap '[ _ prefix >quotation ] map ;
    
    : possibly-true? ( funcs equation -- ? )
      [ numbers>> possible-quotations ] [ value>> ] bi
      '[ call( -- n ) _ = ] any? ;
    
    : solve ( funcs -- n )
      get-input
      [ possibly-true? ] with filter
      [ value>> ] map-sum ;
    
    : part1 ( -- n )
      { + * } solve ;
    
    : _|| ( m n -- mn )
      [ number>string ] bi@ append string>number ;
    
    : part2 ( -- n )
      { + * _|| } solve ;
    
  • stevenviola@programming.dev
    link
    fedilink
    English
    arrow-up
    2
    ·
    20 days ago

    Python

    Takes ~5.3s on my machine to get both outputs. Not sure how to optimize it any further other than running the math in threads? Took me longer than it should have to realize a lot of unnecessary math could be cut if the running total becomes greater than the target while doing the math. Also very happy to see that none of the inputs caused the recursive function to hit Python’s max stack depth.

    Code
    import argparse
    import os
    
    
    class Calibrations:
        def __init__(self, target, operators) -> None:
            self.operators = operators
            self.target = target
            self.target_found = False
    
        def do_math(self, numbers, operation) -> int:
            if operation == "+":
                return numbers[0] + numbers[1]
            elif operation == "*":
                return numbers[0] * numbers[1]
            elif operation == "||":
                return int(str(numbers[0]) + str(numbers[1]))
    
        def all_options(self, numbers, last) -> int:
            if len(numbers) < 1:
                return last
            for j in self.operators:
                # If we found our target already, abort
                # If the last value is greater than the target, abort
                if self.target_found or last > self.target:
                    return
                total = self.all_options(
                    numbers[1:], self.do_math((last, numbers[0]), j)
                )
                if total == self.target:
                    self.target_found = True
    
        def process_line(self, line) -> int:
            numbers = [int(x) for x in line.split(":")[1].strip().split()]
            self.all_options(numbers[1:], numbers[0])
            if self.target_found:
                return self.target
            return 0
    
    
    def main() -> None:
        path = os.path.dirname(os.path.abspath(__file__))
        parser = argparse.ArgumentParser(description="Bridge Repair")
        parser.add_argument("filename", help="Path to the input file")
        args = parser.parse_args()
        sum_of_valid = [0, 0]
        with open(f"{path}/{args.filename}", "r") as f:
            for line in f:
                line = line.strip()
                target = int(line.split(":")[0])
                for idx, ops in enumerate([["+", "*"], ["+", "*", "||"]]):
                    c = Calibrations(target, ops)
                    found = c.process_line(line)
                    sum_of_valid[idx] += found
                    if found:
                        break
        for i in range(0, 2):
            part = i + 1
            print(
                "The sum of valid calibrations for Part "
                + f"{part} is {sum(sum_of_valid[:part])}"
            )
    
    
    if __name__ == "__main__":
        main()
    

    https://github.com/stevenviola/advent-of-code-2024

    • iAvicenna@lemmy.world
      link
      fedilink
      arrow-up
      2
      ·
      edit-2
      19 days ago

      If you havent already done so, doing it in the form of “tree search”, the code completes in the blink of an eye (though on a high end cpu 11th Gen Intel® Core™ i7-11800H @ 2.30GHz). posted the code below

      • stevenviola@programming.dev
        link
        fedilink
        arrow-up
        1
        ·
        20 days ago

        Thanks! yup, I figured there would be a way. You’re right, much faster, on my machine with your code, this is the speed:

        $ time python3 day7.py 
        4555081946288
        227921760109726
        
        real    0m0.171s
        

        I’ll have to take a look to understand how that works to be better.

        • Acters@lemmy.world
          link
          fedilink
          arrow-up
          1
          ·
          edit-2
          19 days ago

          I posted my solution here and found my way to finish 30 milliseconds faster.(~100ms for his, and ~66 ms for mine) However, as I noted I stop prematurely sometimes. Which seems to work with my given input. but here is the one that makes sure it gets to the end of the list of integers:

          code
          def main(input_data):
              input_data = input_data.replace('\r', '')
              parsed_data = {int(line[0]): [int(i) for i in line[1].split()[::-1]] for line in [l.split(': ') for l in input_data.splitlines()]}
              part1 = 0
              part2 = 0
              for item in parsed_data.items():
                  root, num_array = item
                  part_1_branches = [set() for _ in range(len(num_array)+1)]
                  part_2_branches = [set() for _ in range(len(num_array)+1)]
                  part_1_branches[0].add(root)
                  part_2_branches[0].add(root)
                  for level,i in enumerate(num_array):
                      if len(part_1_branches[level]) == 0 and len(part_2_branches[level]) == 0:
                          break
          
                      for branch in part_1_branches[level]:
                          if level==len(num_array)-1:
                              if branch == i:
                                  part1 += root
                                  break
                          if branch % i == 0:
                              part_1_branches[level+1].add(branch//i)
                          if branch - i > 0:
                              part_1_branches[level+1].add(branch-i)
          
                      for branch in part_2_branches[level]:
                          if level==len(num_array)-1:
                              if (branch == i or str(branch) == str(i)):
                                  part2 += root
                                  break
                          if branch % i == 0:
                              part_2_branches[level+1].add(branch//i)
                          if branch - i > 0:
                              part_2_branches[level+1].add(branch-i)
                          if str(i) == str(branch)[-len(str(i)):]:
                              part_2_branches[level+1].add(int(str(branch)[:-len(str(i))].rjust(1,'0')))
              
              print("Part 1:", part1, "\nPart 2:", part2)
              return [part1, part2]
          
          if __name__ == "__main__":
              with open('input', 'r') as f:
                  main(f.read())
          
  • Rin@lemm.ee
    link
    fedilink
    arrow-up
    2
    ·
    20 days ago

    TypeScript

    I wrote my own iterator because I’m a big dummy. Also brute forced (~8s). Might be worth adding a cache to skip all the questions that have been computed / done.

    Solution
    import { AdventOfCodeSolutionFunction } from "./solutions";
    
    function MakeCombination<T>(choices: Array<T>, state: Array<number>): Array<T> {
        return state.map((v) => choices[v]);
    }
    
    function MakeStateArray(length: number) {
        const newArray = [];
        while (length-- > 0)
            newArray.push(0);
    
        return newArray;
    }
    
    function IncrementState(state: Array<number>, max: number): [state: Array<number>, overflow: boolean] {
        state[0]++;
        for (let index = 0; index < state.length; index++) {
            if (state[index] == max) {
                state[index] = 0;
    
                if (index + 1 == state.length)
                    return [state, true];
    
                state[index + 1]++;
            }
        }
    
        return [state, false];
    }
    
    function GenerateCombinations<T>(choices: Array<T>, length: number): Array<Array<T>> {
        const states = MakeStateArray(length);
        const combinations: Array<Array<T>> = [];
    
        let done = false
        while (!done) {
            combinations.push(MakeCombination(choices, states));
            done = IncrementState(states, choices.length)[1];
        }
    
    
        return combinations;
    }
    
    enum Op {
        MUL = "*",
        ADD = "+",
        CON = "|",
    }
    
    function ApplyOp(a: number, b: number, op: Op): number {
        switch (op) {
            case Op.MUL:
                return a * b;
            case Op.ADD:
                return a + b;
            case Op.CON:
                return Number(`${a}${b}`);
        }
    }
    
    function ApplyOperatorsToNumbers(numbers: Array<number>, ops: Array<Op>): number {
        let prev = ApplyOp(numbers[0], numbers[1], ops[0]);
    
        for (let index = 2; index < numbers.length; index++) {
            prev = ApplyOp(prev, numbers[index], ops[index - 1])
        }
    
        return prev;
    }
    
    export const solution_7: AdventOfCodeSolutionFunction = (input) => {
        const numbers = // [{target: 123, numbers: [1, 2, 3, ...]}, ...]
            input.split("\n")
                .map(
                    (v) => v.trim()
                        .split(":")
                        .map(v => v.trim().split(" ").map(v => Number(v)))
                ).map(
                    (v) => {
                        return { target: v[0][0], numbers: v[1] }
                    }
                );
    
        let part_1 = 0;
        let part_2 = 0;
    
        for (let index = 0; index < numbers.length; index++) {
            const target = numbers[index].target;
            const numbs = numbers[index].numbers;
    
            // GenerateCombinations(["+", "*"], 2) => [["+", "+"], ["+", "*"], ["*", "+"], ["*", "*"]]
            const combinations = GenerateCombinations([Op.ADD, Op.MUL], numbs.length - 1); 
    
            // part 1 calculations
            for (let combinationIndex = 0; combinationIndex < combinations.length; combinationIndex++) {
                const combination = combinations[combinationIndex];
                const result = ApplyOperatorsToNumbers(numbs, combination);
                if (result == target) {
                    part_1 += result;
                    break;
                }
            }
    
            const combinations2 = GenerateCombinations([Op.ADD, Op.MUL, Op.CON], numbs.length - 1);
    
            // part 2 calculations
            for (let combinationIndex = 0; combinationIndex < combinations2.length; combinationIndex++) {
                const combination = combinations2[combinationIndex];
                const result = ApplyOperatorsToNumbers(numbs, combination);
                if (result == target) {
                    part_2 += result;
                    break;
                }
            }
    
        }
    
        return {
            part_1,
            part_2,
        }
    }
    
  • Quant@programming.dev
    link
    fedilink
    arrow-up
    1
    ·
    21 days ago

    Uiua

    Credits to @mykl@lemmy.world for the approach of using reduce and also how to split the input by multiple characters.
    I can happily say that I learned quite a bit today, even though the first part made me frustrated enough that I went searching for other approaches ^^

    Part two just needed a simple modification. Changing how the input is parsed and passed to the adapted function took longer than changing the function itself actually.

    Run with example input here

    PartOne ← (
      &rs ∞ &fo "input-7.txt"
      ⊜□≠@\n.
      ≡◇(⊜□≠@:.)
      ≡⍜⊡⋕0
      ≡⍜(°□⊡1)(⊜⋕≠@ .)
      ⟜(⊡0⍉)
    
      # own attempt, produces a too low number
      # ≡(:∩°□°⊟
      #   ⍣(⍤.◡⍣(1⍤.(≤/×)⍤.(≥/+),,)0
      #     ⊙¤⋯⇡ⁿ:2-1⊸⧻
      #     ⊞(⍥(⟜⍜(⊙(↙2))(⨬+×⊙°⊟⊡0)
      #         ↘1
      #       )⧻.
      #       ⍤.=0⧻.
      #     )
      #     ∈♭◌
      #   )0)
    
      # reduce approach found on the programming.dev AoC community by mykl@lemmy.world
      ≡(◇(∈/(◴♭[⊃(+|×)]))⊡0:°⊂)
      °□/+▽
    )
    
    PartTwo ← (
      &rs ∞ &fo "input-7.txt"
      ⊜(□⊜⋕¬∈": ".)≠@\n.
      ⟜≡◇⊢
      ≡◇(∈/(◴♭[≡⊃⊃(+|×|⋕$"__")]):°⊂)
      °□/+▽
    )
    
    &p "Day 7:"
    &pf "Part 1: "
    &p PartOne
    &pf "Part 2: "
    &p PartTwo
    
  • lwhjp@lemmy.sdf.org
    link
    fedilink
    arrow-up
    1
    ·
    edit-2
    21 days ago

    Haskell

    A surprisingly gentle one for the weekend! Avoiding string operations for concatenate got the runtime down below one second on my machine.

    import Control.Arrow
    import Control.Monad
    import Data.List
    import Data.Maybe
    
    readInput :: String -> [(Int, [Int])]
    readInput = lines >>> map (break (== ':') >>> (read *** map read . words . tail))
    
    equatable :: [Int -> Int -> Int] -> (Int, [Int]) -> Bool
    equatable ops (x, y : ys) = elem x $ foldM apply y ys
      where
        apply a y = (\op -> a `op` y) <$> ops
    
    concatenate :: Int -> Int -> Int
    concatenate x y = x * mag y + y
      where
        mag z = fromJust $ find (> z) $ iterate (* 10) 10
    
    main = do
      input <- readInput <$> readFile "input07"
      mapM_
        (print . sum . map fst . (`filter` input) . equatable)
        [ [(+), (*)],
          [(+), (*), concatenate]
        ]
    
    • lwhjp@lemmy.sdf.org
      link
      fedilink
      arrow-up
      1
      ·
      edit-2
      21 days ago

      Since all operations increase the accumulator, I tried putting a guard (a <= x) in apply, but it doesn’t actually help all that much (0.65s -> 0.43s).

      • VegOwOtenks@lemmy.world
        link
        fedilink
        arrow-up
        2
        ·
        edit-2
        20 days ago

        0.65 -> 0.43 sounds pretty strong, isn’t that a one-fourth speedup?

        Edit: I was able to achieve a 30% speed improvement using this on my solution

        • lwhjp@lemmy.sdf.org
          link
          fedilink
          arrow-up
          2
          ·
          20 days ago

          It’s not insignificant, sure, but I’d prefer 10x faster :D

          Plus I’m not sure it’s worth the loss of generality and readability. It is tempting to spend hours chasing this kind of optimization though!

    • VegOwOtenks@lemmy.world
      link
      fedilink
      English
      arrow-up
      0
      ·
      21 days ago

      I wanted to this the way yo did, by repeatedly applying functions, but I didn’t dare to because I like to mess up and spend some minutes debugging signatures, may I ask what your IDE setup is for the LSP-Hints with Haskell?
      Setting up on my PC was a little bit of a pain because it needed matching ghc and ghcide versions, so I hadn’t bothered doing it on my Laptop yet.

      • LeixB@lemmy.world
        link
        fedilink
        arrow-up
        1
        ·
        21 days ago

        I use neovim with haskell-tools.nvim plugin. For ghc, haskell-language-server and others I use nix which, among other benefits makes my development environment reproducible and all haskellPackages are built on the same version so there are no missmatches.

        But, as much as I love nix, there are probably easier ways to setup your environment.

        • VegOwOtenks@lemmy.world
          link
          fedilink
          arrow-up
          1
          ·
          21 days ago

          I just checked and I have haskell-tools.nvim on my PC but it somehow crashes the default config of the autocompletion for me, which I am too inexperienced to debug. I’ll try it nonetheless, since I don’t have autocompletion on the laptop anyways, thank you for the suggestion!

      • lwhjp@lemmy.sdf.org
        link
        fedilink
        arrow-up
        0
        ·
        21 days ago

        Ah, well, I have a bit of a weird setup. GHC is 9.8.4, built from git. I’m using HLS version 2.9.0.1 (again built from git) under Emacs with the LSP and flycheck packages. There are probably much easier ways of getting it to work :)

        • VegOwOtenks@lemmy.world
          link
          fedilink
          arrow-up
          1
          ·
          21 days ago

          I envy emacs for all of its modes, but I don’t think I’m relearning the little I know about vi. Thank you for the answer on the versions and building!

  • RagingHungryPanda@lemm.ee
    link
    fedilink
    English
    arrow-up
    1
    ·
    7 days ago

    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#

    // 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”.

    Method Mean Error StdDev Gen0 Gen1 Allocated
    Part1 19.20 ms 0.372 ms 0.545 ms 17843.7500 156.2500 106.55 MB
    Part2 17.94 ms 0.355 ms 0.878 ms 17843.7500 156.2500 106.55 MB

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

    Method Mean Error StdDev Gen0 Gen1 Allocated
    Part1 17.34 ms 0.342 ms 0.336 ms 17843.7500 125.0000 106.55 MB
    Part2 17.24 ms 0.323 ms 0.270 ms 17843.7500 93.7500 106.55 MB
  • Karmmah@lemmy.world
    link
    fedilink
    arrow-up
    1
    ·
    20 days ago

    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.

    Code
    function readInput(inputFile::String)::Vector{Vector{Int}}
    	f = open(inputFile,"r")
    	lines::Vector{String} = readlines(f)
    	close(f)
    	equations::Vector{Vector{Int}} = []
    	function getValues(line::String)
    		return map(sp->parse(Int,sp),(sp=split(line," ");sp[1]=sp[1][1:end-1];sp))
    	end
    	map(l->push!(equations,getValues(l)),lines)
    	return equations
    end
    
    function checkEq(eq::Vector{Int},withConCat::Bool)::Bool
    	function calcEq(eq::Vector{Int},operators::Vector{Int},withConCat::Bool)::Int
    		res::Int = eq[2]
    		for (i,op) in enumerate(operators)
    			if op == 0 #+
    				res += eq[i+2]
    			elseif op ==1 #*
    				res *= eq[i+2]
    			else #op==2 ||
    				res = parse(Int,string(res)*string(eq[i+2]))
    			end
    		end
    		return res
    	end
    	opInt::Int = 0
    	operators = Vector{Int}(undef,length(eq)-2)
    	while opInt < (withConCat ? 3^(length(eq)-2) : 2^(length(eq)-2))
    		withConCat==true ? operators=digits(opInt,base=3,pad=length(eq)-2) : operators=digits(opInt,base=2,pad=length(eq)-2)
    		#calcEq(eq,operators,withConCat)==eq[1] ? (return true) : opInt -= 1
    		calcEq(eq,operators,withConCat)==eq[1] ? (return true) : opInt += 1
    	end
    	return false
    end
    
    function calcTotCalRes(equations::Vector{Vector{Int}},withConCat::Bool)::Int
    	totCalRes::Int = 0
    	for e in equations
    		checkEq(e,withConCat) ? totCalRes+=e[1] : nothing
    	end
    	return totCalRes
    end
    
    @info "Part 1"
    println("result: $(calcTotCalRes(readInput("day07Input"),false))")
    @info "Part 2"
    println("result: $(calcTotCalRes(readInput("day07Input"),true))")
    
  • the_beber@lemm.ee
    link
    fedilink
    arrow-up
    1
    ·
    edit-2
    18 days ago

    Kotlin

    I finally got around to doing day 7. I try the brute force method (takes several seconds), but I’m particularly proud of my sequence generator for operation permutations.

    The Collection#rotate method is in the file Utils.kt, which can be found in my repo.

    Solution
    import kotlin.collections.any
    import kotlin.math.pow
    
    fun main() {
        fun part1(input: List<String>): Long {
            val operations = setOf(CalibrationOperation.Plus, CalibrationOperation.Multiply)
            return generalizedSolution(input, operations)
        }
    
        fun part2(input: List<String>): Long {
            val operations = setOf(CalibrationOperation.Plus, CalibrationOperation.Multiply, CalibrationOperation.Concat)
            return generalizedSolution(input, operations)
        }
    
        val testInput = readInput("Day07_test")
        check(part1(testInput) == 3749L)
        check(part2(testInput) == 11387L)
    
        val input = readInput("Day07")
        part1(input).println()
        part2(input).println()
    }
    
    fun parseInputDay7(input: List<String>) = input.map {
        val calibrationResultAndInput = it.split(':')
        calibrationResultAndInput[0].toLong() to calibrationResultAndInput[1].split(' ').filter { it != "" }.map { it.toLong() }
    }
    
    fun generalizedSolution(input: List<String>, operations: Set<CalibrationOperation>): Long {
        val parsedInput = parseInputDay7(input)
        val operationsPermutations = CalibrationOperation.operationPermutationSequence(*operations.toTypedArray()).take(calculatePermutationsNeeded(parsedInput, operations)).toList()
        return sumOfPossibleCalibrationEquations(parsedInput, operationsPermutations)
    }
    
    fun calculatePermutationsNeeded(parsedInput: List<Pair<Long, List<Long>>>, operations: Set<CalibrationOperation>): Int {
        val highestNumberOfOperations = parsedInput.maxOf { it.second.size - 1 }
        return (1..highestNumberOfOperations).sumOf { operations.size.toDouble().pow(it).toInt() }
    }
    
    fun sumOfPossibleCalibrationEquations(parsedInput: List<Pair<Long, List<Long>>>, operationPermutationCollection: Collection<OperationPermutation>): Long {
        val permutationsGrouped = operationPermutationCollection.groupBy { it.size }
        return parsedInput.sumOf { (equationResult, equationInput) ->
            if (permutationsGrouped[equationInput.size - 1]!!.any { operations ->
                    equationResult == equationInput.drop(1)
                        .foldIndexed(equationInput[0]) { index, acc, lng -> operations[index](acc, lng) }
                }) equationResult else 0
        }
    }
    
    typealias OperationPermutation = List<CalibrationOperation>
    
    sealed class CalibrationOperation(val operation: (Long, Long) -> Long) {
        operator fun invoke(a: Long, b: Long) = operation(a, b)
        object Plus : CalibrationOperation({ a: Long, b: Long -> a + b })
        object Multiply : CalibrationOperation({ a: Long, b: Long -> a * b })
        object Concat : CalibrationOperation({ a: Long, b: Long -> "$a$b".toLong() })
    
        companion object {
            fun operationPermutationSequence(vararg operations: CalibrationOperation) = sequence<OperationPermutation> {
                val cache = mutableListOf<OperationPermutation>()
                val calculateCacheRange = { currentLength: Int ->
                    val sectionSize = operations.size.toDouble().pow(currentLength - 1).toInt()
                    val sectionStart = (1 until currentLength - 1).sumOf { operations.size.toDouble().pow(it).toInt() }
                    sectionStart..(sectionStart + sectionSize - 1)
                }
    
                // Populate the cache with initial values for permutation length 1.
                operations.forEach { operation -> yield(listOf(operation).also { cache.add(it) }) }
    
                var currentLength = 2
                var offset = 0
                var cacheRange = calculateCacheRange(currentLength)
                var rotatingOperations = operations.toList()
                yieldAll(
                    generateSequence {
                        if (cacheRange.count() == offset) {
                            rotatingOperations = rotatingOperations.rotated(1)
                            if (rotatingOperations.first() == operations.first()) {
                                currentLength++
                            }
    
                            offset = 0
                            cacheRange = calculateCacheRange(currentLength)
                        }
    
                        val cacheSlice = cache.slice(cacheRange)
    
                        return@generateSequence (cacheSlice[offset] + rotatingOperations.first()).also {
                            cache += it
                            offset++
                        } 
                    }
                )
            }
        }
    }
    
    

  • janAkali@lemmy.one
    link
    fedilink
    English
    arrow-up
    0
    ·
    edit-2
    21 days ago

    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

    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
    

    Codeberg repo

  • Ananace@lemmy.ananace.dev
    link
    fedilink
    arrow-up
    0
    ·
    edit-2
    21 days ago

    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.

    C#
    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);
    }
    
  • TunaCowboy@lemmy.world
    link
    fedilink
    arrow-up
    0
    ·
    edit-2
    21 days ago

    python

    45s on my machine for first shot, trying to break my will to brute force 😅. I’ll try improving on it in a bit after I smoke another bowl and grab another drink.

    solution
    import itertools
    import re
    import aoc
    
    def ltr(e):
        r = int(e[0])
        for i in range(1, len(e), 2):
            o = e[i]
            n = int(e[i + 1])
            if o == '+':
                r += n
            elif o == '*':
                r *= n
            elif o == '||':
                r = int(f"{r}{n}")
        return r
    
    def pl(l, os):
        d = [int(x) for x in re.findall(r'\d+', l)]
        t, ns = d[0], d[1:]
        ops = list(itertools.product(os, repeat=len(ns) - 1))
        for o in ops:
            e = str(ns[0])
            for i, op in enumerate(o):
                e += f" {op} {ns[i + 1]}"
            r = ltr(e.split())
            if r == t:
                return r
        return 0
    
    def one():
        lines = aoc.get_lines(7)
        acc = 0
        for l in lines:
            acc += pl(l, ['+', '*'])
        print(acc)
    
    def two():
        lines = aoc.get_lines(7)
        acc = 0
        for l in lines:
            acc += pl(l, ['+', '*', '||'])
        print(acc)
    
    one()
    two()
    
      • TunaCowboy@lemmy.world
        link
        fedilink
        arrow-up
        1
        ·
        edit-2
        20 days ago

        It’s not a long lived project, it’s a puzzle, and once solved never needs to run again. My objective here is to get the correct answer, not win a style contest.

        Can you provide a link to your solution? I’d like to check it out.

        • Katzenmann@feddit.org
          link
          fedilink
          English
          arrow-up
          1
          ·
          edit-2
          20 days ago

          My initial comment was a bit harsh, I’m sorry for that. It was meant to be a bit of a joke. Anyway here’s my code. Do note that I don’t do the challenges timed so I have a bit more time to name my variables accordingly. Takes 35 seconds to run on a pc with a AMD Ryzen 5 5600

          import sys
          from tqdm import tqdm
          
          
          input = sys.stdin.read()
          
          def all_operator_permutations(operator_count):
              if operator_count == 0:
                  return [[]]
          
              smaller_permutations = all_operator_permutations(operator_count-1)
              return [
                      *[['+', *ops] for ops in smaller_permutations],
                      *[['*', *ops] for ops in smaller_permutations],
                      *[['||', *ops] for ops in smaller_permutations],
                      ]
          
          def test_operators(ops, values):
              res = values.pop(0)
              for op in ops:
                  match op:
                      case '*':
                          res *= values.pop(0)
                      case '+':
                          res += values.pop(0)
                      case '||':
                          res = int(f"{res}{values.pop(0)}")
              return res
          
          
          total_calibration_result = 0
          
          for line in tqdm(input.splitlines()[:]):
              target, *tail = line.split(':')
              target = int(target)
              values = [int(val) for val in tail[0].split()]
          
              all_perms = all_operator_permutations(len(values) - 1)
              ops = all_perms.pop()
              while True:
                  res = test_operators(ops, values.copy())
                  if res == target:
                      total_calibration_result += target
                      break
                  if not all_perms:
                      break
                  ops = all_perms.pop()
          
          print(total_calibration_result)