[–] [S] 2 points 2 months ago* (last edited 2 months ago)

Python

Made a single cleaned-up solver for all 3 parts after solving them separately. Intersection logic can be optimized, but the inputs are small enough to not need it.

click to view code

# simple type to represent an arc
Arc = tuple[int, int]

# single solver for all 3 parts
class Solver:
    def __init__(self, forward_skips_visited = False, forbid_crossings = False):
        # a forward jump landing on a visited point slides right to the first free point (part 2)
        self.forward_skips_visited = forward_skips_visited
        # arcs may not cross, and a jump with no valid landing is skipped entirely (part 3)
        self.forbid_crossings = forbid_crossings

        # current position on the line
        self.pos = 0
        # set of visited points on the line
        self.visited = {0}
        # which side the arc will be drawn on next (down = 0, up = 1)
        self.side = 0
        # arc segments on each side of the line
        self.arcs: dict[int, list[Arc]] = { 0: [], 1: [] }

    # get the first arc / segment that crosses the given range
    def _get_crossing(self, start: int, end: int) -> Arc | None:
        low, high = min(start, end), max(start, end)
        for a, b in self.arcs[self.side]:
            if a < low < b < high or low < a < high < b:
                return a, b
        return None

    # try moving backwards by the given length, return None if not possible
    def _move_backward(self, length: int) -> int | None:
        next_pos = self.pos - length
        if next_pos < 0 or next_pos in self.visited:
            return None
        if self.forbid_crossings and self._get_crossing(next_pos, self.pos) is not None:
            return None
        return next_pos

    # try moving forwards by the given length, return None if not possible
    def _move_forward(self, length: int) -> int | None:
        next_pos = self.pos + length
        while True:
            # keep moving forward until an unvisited point is found
            if self.forward_skips_visited and next_pos in self.visited:
                next_pos += 1
                continue

            if not self.forbid_crossings:
                return next_pos

            crossing = self._get_crossing(self.pos, next_pos)
            if crossing is None:
                return next_pos

            # if the crossing arc ends before the next position, it becomes impossible to jump without crossing it
            _, end = crossing
            if end < next_pos:
                return None
            
            # clear the arc
            next_pos = end + 1

    # perform a jump of the given length according to the active rules
    def jump(self, length: int):
        next_pos = self._move_backward(length)
        if next_pos is None:
            next_pos = self._move_forward(length)
        if next_pos is None:
            return

        # update solver state
        self.arcs[self.side].append((min(self.pos, next_pos), max(self.pos, next_pos)))
        self.visited.add(next_pos)
        self.side = 1 - self.side
        self.pos = next_pos

def sum_final_positions(data: str, forward_skips_visited = False, forbid_crossings = False):
    total = 0
    for line in data.splitlines():
        solver = Solver(forward_skips_visited, forbid_crossings)
        for length in map(int, line.split(',')):
            solver.jump(length)
        total += solver.pos
    return total

def part1(data: str):
    """
    Rules:
    - For each jump, first try to move backwards by its specified length.
    - If the destination is negative or has been visited before, move forwards by the same distance.
    """
    return sum_final_positions(data)

def part2(data: str):
    """
    Additional rules:
    - Whenever a forward jump would land on a previously visited point,
        increase the destination by one until you reach the first unvisited point
    """
    return sum_final_positions(data, forward_skips_visited=True)

def part3(data: str):
    """
    Additional rules:
    - If a backward jump would cause a crossing, try moving forwards instead
    - If a forward jump would cause a crossing, keep increasing its length by one until there is no crossing
    - If no valid forward jump exists, skip that jump entirely and continue with the next jump length in the sequence
    """
    return sum_final_positions(data, forward_skips_visited=True, forbid_crossings=True)

  • source
  • [–] 2 points 2 months ago

    Python

    The DP solution to go column by column and compute the min flaps for each cell is correct but too slow. Fortunately this problem can be solved using MATH

    # Uses math to calculate the minimum number of flaps needed to reach the last passage at any height.
    # This solution relies on a couple of facts:
    #   1. The number of flaps needed to reach a passage at (x, y) is ceil((x + y) / 2).
    #       Proof:
    #       - Each flap increases your height by 1 and each glide decreases it by 1
    #       - Suppose you flapped f times and covered a horizontal distance of x
    #       - So your final height would be y = f - (x - f) = 2f - x
    #       - Now suppose a passage at position p_x begins at height p_y. To enter it, your height must be, y >= p_y
    #       - Substituting y, 2f - x >= p_y => 2f - p_x >= p_y
    #       - Solving for f, f >= (p_x + p_y) / 2
    #       - Since f is an integer, f = ceil((p_x + p_y) / 2)
    #   2. For each wall, the lowest opening gives the smallest lower bound on the cumulative number of flaps.
    #      The maximum of these bounds is necessary because an earlier high wall may require more flaps than the last wall.
    #   3. For this input, that maximum lower bound is attainable through all the walls, so it is the optimal answer.
    #      This is not true in general: an opening's upper edge or the distance between walls can make the lowest opening unreachable.
    def flapsMath(data: str):
        flaps = 0
    
        passages = [[int(p) for p in passage.split(',')] for passage in data.splitlines()]
        passages.sort(key=lambda p: (p[0], p[1]))  # Sort passages so that we always have the lowest opening first
        last_seen_x = 0
    
        for x, y, _ in passages:
            if x != last_seen_x:
                last_seen_x = x
                # calculate the minimum number of flaps needed to cross this passage at its lowest opening
                # (x + y + 1) // 2 is equivalent to ceil((x + y) / 2) for integers
                flaps = max(flaps, (x + y + 1) // 2)
    
        return flaps
    
  • source
  • [–] 2 points 2 months ago

    Python

    Couldn't finish the series when it was released but I'm returning to finish it now. The set of free branches of part3 is too large for brute-force but you can exploit the quirk in the input where each free branch only contributes positively or negatively.

    from collections import defaultdict
    from dataclasses import dataclass
    import re
    
    # regex to match numbers in the input data
    MATCH_NUMS_PATTERN = re.compile(r"(-?\d+)")
    
    # Plant state class
    @dataclass
    class Plant:
        id: int
        thickness: int
        # is_free indicates whether the plant has a free branch.
        # it is also used to turn the effect of free branches on or off.
        is_free: bool = False
    
    # Parses the plant input data into a list of Plant objects and a graph representing the connections between plants
    # The graph root is the the final plant and the leaves are the free branches. 
    def parse_plants(data: str):
        plants: list[Plant] = []
        graph = defaultdict(list)
    
        # Divide the input into blocks for each plant
        for block in data.split("\n\n"):
            # line iterator to control consumption of lines in the block 
            lines_iter = iter(block.splitlines())
    
            # get the plant's id and thickness from the first line of the block
            id, thickness = map(int, re.findall(MATCH_NUMS_PATTERN, next(lines_iter)))
            curr_plant = Plant(id, thickness)
            plants.append(curr_plant)
    
            # parse the remaining lines in the block to get the plant's branches
            for line in lines_iter:
                if line.startswith("- free"):
                    curr_plant.is_free = True
                else:
                    from_plant, thickness = map(int, re.findall(MATCH_NUMS_PATTERN, line))
                    graph[curr_plant.id].append((from_plant, thickness))
        
        return plants, graph
    
    # Recursively calculates the energy for a given plant.
    # Naive implementation with no memoization, but enough for the input size.
    def get_energy_at(plants: list[Plant], graph: dict[int, list[tuple[int, int]]], plant_id: int):
        plant: Plant = plants[plant_id-1]
        energy = 0
    
        # if the plant has a free branch, its energy is equal to its thickness.
        # otherwise, its energy is the sum of the incoming energy from its branches, multiplied by the thickness of each branch.
        if plant.is_free:
            energy = plant.thickness
        else:
            for from_plant, thickness in graph[plant_id]:
                energy += thickness * get_energy_at(plants, graph, from_plant)
    
        # energy only moves through the plant if it is less than or equal to the plant's thickness.
        return energy if plant.thickness <= energy else 0
    
    # Part 1 is simple: just calculate the energy at the final plant with all free branches on.
    def part1(data: str) -> int:
        plants, graph = parse_plants(data)
        return get_energy_at(plants, graph, len(plants))
    
    # Part 2: use the boolean data to turn free branches on or off and calculate the energy at the final plant for each configuration.
    def part2(data: str) -> int:
        # split the input data into plant data and boolean data
        plant_data, bool_data = data.split("\n\n\n")
        plants, graph = parse_plants(plant_data)
    
        all_energy = 0
        for line in bool_data.splitlines():
            # transform the boolean string into a list of integers and set the is_free attribute of each plant accordingly
            bools = map(int, line.split(' '))
            for i, b in enumerate(bools):
                plants[i].is_free = b == 1
    
            all_energy += get_energy_at(plants, graph, len(plants))
        return all_energy
    
    # Part 3: calculate the maximum possible energy at the final plant, 
    #   then calculate the cumulative difference in energy between the maximum and each provided configuration of free branches.
    # To calculate the maximum possible energy:
    #   First, I tried to progressively turn free plants off or on but that doesn't work and the energy stays at 0
    #   Since this is a set of constraints, this can be solved by SMT solvers like z3
    #   However, there is a quirk in the input data that allows for a simpler solution:
    #       Each free branch contributes either positively or negatively ONLY
    #       So we can simply turn off all free branches that contribute negatively and get the max energy.
    # I don't like this solution because it relies on a quirk in the input data and doesn't work for all inputs,
    #   even the sample data
    def part3(data: str) -> int:
        # split the input data into plant data and boolean data
        plant_data, bool_data = data.split("\n\n\n")
        plants, graph = parse_plants(plant_data)
    
        # calculate the contribution of each free branch
        plant_contrib = defaultdict(int)
        for plant in plants:
            # free branches won't have any outgoing edges
            if plant.is_free:
                continue
    
            # for a non-leaf plant, we cumulate the contribution of each of its free branches
            for from_plant, thickness in graph[plant.id]:
                if not plants[from_plant-1].is_free:
                    continue
    
                # assert our assumption about the input data that 
                #   each free branch contributes either positively or negatively ONLY
                if plant_contrib[from_plant]:
                    assert (plant_contrib[from_plant] < 0) == (thickness < 0), (
                        "this approach only works if all free branches contribute "
                        "either positively or negatively ONLY"
                    )
                
                plant_contrib[from_plant] += thickness
    
        # turn off all free branches that contribute negatively
        for id, contrib in plant_contrib.items():
            if contrib >= 0:
                continue
            plants[id-1].is_free = False
    
        # get max energy for this configuration
        max_energy = get_energy_at(plants, graph, len(plants))
    
        # calculate the cumulative difference in energy between the maximum and 
        #   each provided configuration of free branches.
        energy_diff = 0
        for line in bool_data.splitlines():
            bools = map(int, line.split(' '))
            for i, b in enumerate(bools):
                plants[i].is_free = b == 1
    
            dd_energy = get_energy_at(plants, graph, len(plants))
            # we skip configurations that do not activate the final plant
            if dd_energy == 0:
                continue
            energy_diff += max_energy - dd_energy
        
        return energy_diff
    
  • source
  • [–] 2 points 8 months ago

    Uiua

    I'm currently going through the 2025 Quests to learn Rust, committing great sins along the way.
    Decided to take a shot at the third one with Uiua because it looked nice and simple, and it was. This may have been the fastest I've ever finished a complete quest :P

    Run with example input

    Code

    I₁   ← "10,5,1,10,3,8,5,2,2"
    Iβ‚‚   ← "4,51,13,64,57,51,82,57,16,88,89,48,32,49,49,2,84,65,49,43,9,13,2,3,75,72,63,48,61,14,40,77"
    I₃   ← "4,51,13,64,57,51,82,57,16,88,89,48,32,49,49,2,84,65,49,43,9,13,2,3,75,72,63,48,61,14,40,77"
    Prep ← βŠβŠΈβ–βŠœβ‹•βŠΈβ‰ @,
    P₁   ← /+β—΄ Prep
    Pβ‚‚   ← /+↙₋₂₀◴ Prep
    P₃   ← +₁/β†₯⧆ Prep
    ⍚$"Part _: _" 1_2_3 [P₁I₁ Pβ‚‚Iβ‚‚ P₃I₃]
    &pf /$$ _
         $$ _
    

  • source
  • [–] [S] 1 point 8 months ago

    Futhark + sed

    Futhark can read only number and array types from stdin, the input data is often a non-starter. Therefore, I often massage it into the right format using sed.

    Sed Script

    s/[: ]/,/g
    s/[rgbs]/0/g
    s/[RGBS]/1/g
    1s/^/[ [/
    2,$s/^/, [/
    s/$/]/
    s/,\([01]\)/,0b\1/g
    
    $a]
    

    This formats everything nicely, e.g. the example for part 1:

    2456:rrrrrr ggGgGG bbbbBB
    7689:rrRrrr ggGggg bbbBBB
    3145:rrRrRr gggGgg bbbbBB
    6710:rrrRRr ggGGGg bbBBbB
    

    is transformed into this:

    [ [2456,0b000000,0b001011,0b000011]
    , [7689,0b001000,0b001000,0b000111]
    , [3145,0b001010,0b000100,0b000011]
    , [6710,0b000110,0b001110,0b001101]
    ]
    

    Futhark Solution

    entry part1 (colors: [][4]i32) =
      let isGreenDominant c = c.g > c.r && c.g > c.b
      in colors
         |> map (\array -> (array[0], {r = array[1], g = array[2], b = array[3]}))
         |> filter ((.1) >-> isGreenDominant)
         |> map (.0)
         |> i32.sum
    
    type Optional 'a = #absent | #present a
    
    def map_optional 'a 'b (f: a -> b) (this: Optional a) : Optional b =
      match this
      case #absent -> #absent
      case #present a -> #present (f a)
    
    def optional_is_present 'a (this: Optional a) : bool =
      match this
      case #absent -> false
      case _ -> true
    
    def unwrap_optional 'a (this: Optional a) : a =
      match this
      case #absent -> assert (("unwrap_optional: #absent", false).1) ([][1])
      case #present a -> a
    
    def bind_optional 'a 'b (f: a -> Optional b) (this: Optional a) : Optional b =
      match this
      case #absent -> #absent
      case #present a -> f a
    
    def maximum_by 'a (gte: a -> a -> bool) (as: []a) : Optional a =
      let choose (this: Optional a) (other: Optional a) =
        match (this, other)
        case (#absent, o) -> o
        case (t, #absent) -> t
        case (#present t, #present o) -> #present (if o `gte` t then o else t)
      in reduce choose #absent (map (\a -> #present a) as)
    
    type ShineColor = {r: i32, g: i32, b: i32, s: i32}
    
    def array2ShineColor (array: [5]i32) : (i32, ShineColor) = (array[0], {r = array[1], g = array[2], b = array[3], s = array[4]})
    
    def on f g a b = f (g a) (g b)
    
    entry part2 (colors: [][5]i32) =
      let gte (this: ShineColor) (other: ShineColor): bool =
        let colorSum c = c.r + c.g + c.b
        in this.s > other.s || (this.s == other.s && colorSum this <= colorSum other)
      in colors
         |> map array2ShineColor
         |> maximum_by (gte `on` (.1))
         |> unwrap_optional
         |> (.0)
    
    type Shininess = #matte | #shiny
    type DominantColor = #red | #green | #blue
    
    def filterMap 'a 'b [n] (f: a -> Optional b) (as: [n]a) : []b =
      as
      |> map f
      |> filter optional_is_present
      |> map unwrap_optional
    
    def categorize_shininess (s: i32) : Optional Shininess =
      if s <= 30 then #present #matte else if s >= 33 then #present #shiny else #absent
    
    def categorize_color (color: ShineColor) : Optional DominantColor =
      if color.r > color.g && color.r > color.b
      then #present #red
      else if color.g > color.r && color.g > color.b
      then #present #green
      else if color.b > color.r && color.b > color.g
      then #present #blue
      else #absent
    
    def categorize ((value, color): (i32, ShineColor)) : Optional (i32, (DominantColor, Shininess)) =
      categorize_shininess color.s
      |> bind_optional (\s -> map_optional (\c -> (c, s)) (categorize_color color))
      |> map_optional (\g -> (value, g))
    
    -- >>> category_index (#red, #shiny)
    -- 3
    
    def category_index ((c, s): (DominantColor, Shininess)) : i64 =
      (+) (match c
           case #red -> 0
           case #green -> 1
           case #blue ->
             2)
          (match s
           case #matte -> 0
           case #shiny -> 3)
    
    def (&&&) f g x = (f x, g x)
    def sum2d (a, b) (c, d) : (i32, i32) = (a + c, b + d)
    
    entry part3 (colors: [][5]i32) =
      colors
      |> map array2ShineColor
      |> filterMap categorize
      |> map ((.1) >-> category_index &&& ((.0) &&& const 1))
      |> unzip
      |> uncurry (hist sum2d (0, 0) 6)
      |> maximum_by ((>=) `on` (.1))
      |> unwrap_optional
      |> (.0)
    

    I had hoped that the input might get big enough for optimization purposes, but the calculations are likely too simple.

  • source
  • [–] 2 points 9 months ago

    Guile Scheme

    Runs in 3.2 seconds.

    I quickly got part 2 two to work on the example input, but i struggled a lot with bugs in the polygon-test code before getting the right result. To find my bugs, i made a visualization using raylib. It's not part of the snippet, but you can find the full code here.

    (define-module (day9)
      #:use-module (input)
      #:use-module (util)
      #:use-module (srfi srfi-1)
      #:use-module (ice-9 match)
      #:use-module (ice-9 control)
      #:use-module (system foreign)
      #:use-module (rnrs bytevectors)
      #:use-module (raylib)
      #:export (day))
    
    (define (parse-coord str)
      (map string->number (string-split str #\,)))
    
    (define (parse port)
      (-> (read-lines port)
          (curry map string-trim-both)
          (curry filter (negate string-empty?))
          (curry map parse-coord)
          (curry map (match-lambda [(a b) (cons a b)]))))
    
    (define (area a b)
      (match-let ([(ax . ay) a] [(bx . by) b])
                 (* (+ 1 (abs (- bx ax)))
                    (+ 1 (abs (- by ay))))))
    
    (define (is-in-polygon pos corners)
      (match-let ([(x . y) pos])
        (call/ec (Ξ» (return)
          (odd? (let go ([corners (cons (last corners) corners)])
            (match corners
              [() 0]
              [(_) 0]
              [((bx . by) (ax . ay) . rest)
               (+ (call/ec (Ξ» (next)
                              (cond
                                [(= bx ax)
                                 (when (< y (min ay by))
                                   (next 0))
                                 (when (>= y (max ay by))
                                   (next 0))
                                 (when (= x ax)
                                   (return #t))
                                 (when (< x ax)
                                   (next 1))
                                 (when (> x ax)
                                   (next 0))]
                                [(= by ay)
                                 (when (not (= y ay))
                                   (next 0))
                                 (when (< x (min ax bx))
                                   (next 0))
                                 (when (> x (max ax bx))
                                   (next 0))
                                 (return #t)])))
                                 (go (cons `(,ax . ,ay) rest)))])))))))
    
    (define (lines-intersect? a b)
      (match-let* ([((asx . asy) . (aex . aey)) a]
                   [((bsx . bsy) . (bex . bey)) b]
                   [alx (min asx aex)]
                   [ahx (max asx aex)]
                   [blx (min bsx bex)]
                   [bhx (max bsx bex)]
                   [aly (min asy aey)]
                   [ahy (max asy aey)]
                   [bly (min bsy bey)]
                   [bhy (max bsy bey)]
                   [a-horizontal (= asy aey)]
                   [b-horizontal (= bsy bey)])
        (and
          (not (equal? a-horizontal b-horizontal))
          (if a-horizontal
            (and
              (<= alx blx)
              (>= ahx blx)
              (> aly bly)
              (< ahy bhy))
            (lines-intersect? b a)))))
    
    (define (line-intersects-polygon? line corners)
      (let go ([corners (cons (last corners) corners)])
        (match corners
            [() #f]
            [(a) #f]
            [(a b . rest)
             (or
               (lines-intersect? line `(,a . ,b))
               (go (cons b rest)))])))
    
    (define (rectangle-corners a b)
      (match-let ([(ax . ay) a] [(bx . by) b])
        `((,(min ax bx) . ,(min ay by))
          (,(min ax bx) . ,(max ay by))
          (,(max ax bx) . ,(min ay by))
          (,(max ax bx) . ,(max ay by)))))
    
    (define (rectangle-edges a b)
      (match-let ([(tl bl tr br) (rectangle-corners a b)])
        `((,tl . ,tr)
          (,tr . ,br)
          (,br . ,bl)
          (,bl . ,tl))))
    
    (define-day day 9 'real
      (Ξ» (port called-directly)
         (define red-tiles (parse port))
    
         (define is-in-loop? (cached (Ξ» (pos) (is-in-polygon pos red-tiles))))
         (define intersects-loop? (cached (Ξ» (line) (line-intersects-polygon? line red-tiles))))
    
         (define tile-combinations (pairs red-tiles))
    
         (define part1 (-> tile-combinations
                           (curry map (match-lambda [(a . b) (area a b)]))
                           (sort _ >)
                           car))
    
         (define part2 (-> tile-combinations
                           (curry filter (match-lambda [(a . b)
                                                        (and (all is-in-loop? (rectangle-corners a b))
                                                             (all (Ξ» (edge) (not (intersects-loop? edge))) (rectangle-edges a b)))]))
                           (curry map (match-lambda [(a . b) `(,(area a b) . (,a . ,b))]))
                           (sort _ (lambda (a b) (on car > a b)))
                           car))
    
         (when called-directly (visualization red-tiles (cdr part2)))
    
         `(,part1 ,(car part2))))
    
  • source
  • [–] 3 points 9 months ago (2 children)

    I know I'm late, but I'd thought, I'd share some of the visualizations I wanted to see. The code is woefully inefficient running on a 2D array with some greedy heuristics

    Here's my shapes with symmetries:

    Here's some of the solutions:





    The visuals are terminal based, for simple cells I used two spaces with colored background and for solutions I also used braille symbols to better delineate distinctions between cells.

  • source
  • [–] 2 points 9 months ago* (last edited 9 months ago)

    I know I'm late, but I'd thought, I'd share some of my day 12 visualizations I wanted to see.

    Here's my shapes with symmetries:

    Here's some of the solutions found in a greedy manner:





    The visuals are terminal based, for simple cells I used two spaces with colored background and for solutions I also used braille symbols to better delineate distinctions between cells.

  • source
  • [–] [S] 4 points 10 months ago

    For a final bit of code to roast, here is the bot that posted the daily threads:

    Bot code

    use std::process::exit;
    use chrono::{Datelike, Timelike};
    use lemmy_client::lemmy_api_common::community::GetCommunity;
    use lemmy_client::lemmy_api_common::person::Login;
    use lemmy_client::lemmy_api_common::post::CreatePost;
    use lemmy_client::{ClientOptions, LemmyClient};
    use rand::seq::IndexedRandom;
    use regex::Regex;
    
    static TARGET_COMMUNITY: &str = "Advent_Of_Code@programming.dev";
    
    async fn get_advent_emoji() -> String {
        let choices = ['πŸŽ„','🎁','🧦','πŸ•―','🌟','✨','🧸','β›„','β˜ƒ','❄','πŸ”','πŸŽ…','🀢','πŸ§‘','🦌','πŸ‘Ό','πŸ‘ͺ','🍽','πŸ₯›','🍷','πŸ—','πŸ₯§','🍬','🍫','🎢','🎢','πŸ””','🎊','πŸŽ‰','πŸ’ƒ'];
        choices.choose(&mut rand::rng()).unwrap().to_string()
    }
    
    async fn get_advent_title(year: i32, day: u32) -> String {
        let url = format!("https://adventofcode.com/%7B%7D/day/%7B%7D", year, day);
    
        let resp = reqwest::get(url).await.unwrap();
        let body = String::from_utf8(resp.bytes().await.unwrap().to_vec()).unwrap();
    
        let re = Regex::new(r"---(.*)---").unwrap();
    
        for (_, [line]) in re.captures_iter(&body).map(|c| c.extract()) {
            return line.trim().to_string();
        }
    
        panic!("could not find advent title - {body}");
    }
    
    #[tokio::main]
    async fn main() {
        let time = chrono::Utc::now();
        let year = time.year();
        let day = time.day();
    
        if time.hour() != 5 {
            println!("Wrong time: {time}");
            exit(0);
        }
    
        let title = get_advent_title(year, day).await;
        println!("Title: {:#?}", title);
    
        post_to_lemmy(year, day, title).await;
    }
    
    async fn post_to_lemmy(year: i32, day:u32, challenge_title: String) {
        let emoji = get_advent_emoji().await;
        let post_title = format!("{emoji} - {year} DAY {day} SOLUTIONS - {emoji}");
        let post_body = format!("# {challenge_title}
    
    ## 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
    - What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
    - Where do I participate?: https://adventofcode.com/
    - Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465");
    
        let mut client = LemmyClient::new(ClientOptions {
            domain: "programming.dev".to_string(),
            secure: true,
        });
    
        let auth = client.login(Login {
            username_or_email: "HARDCODED".to_string().into(),
            password: "HARDCODED".to_string().into(),
            totp_2fa_token: None,
        }).await.unwrap();
    
        client.headers_mut().insert("Authorization".to_string(),
                                    format!("Bearer {}", auth.jwt.unwrap().to_string()));
    
        let community = client.get_community(GetCommunity {
            id: None,
            name: Some(TARGET_COMMUNITY.to_string()),
        }).await.unwrap();
        let community_id = community.community_view.community.id;
    
        client.create_post(CreatePost {
            name: post_title,
            community_id,
            url: None,
            body: Some(post_body),
            alt_text: None,
            honeypot: None,
            nsfw: None,
            language_id: None,
            custom_thumbnail: None,
        }).await.unwrap();
    
    }
    

  • source
  • [–] 2 points 10 months ago

    Python

    I know I'm late to the party, but I fucking did it. Fuck this was hard. It didn't help me, that I decided not to use any external libs in my solutions. It does part two in ~5 s.

    Yes, I'm proud of myself. No, the code is not good by any means.

    The solution itself:

    1. Breadth first search with condition that every button will be pressed at most once;
    2. There were several approaches:
      • A* β€” no luck;
      • What ever the fuck I did:
        1. Gauss-Jordan reduction;
        2. Simplex to optimize reduced problem, if previous step doesn't provide a full solution;
        3. On top of all that β€” branch and bound search to resolve cases where optimization doesn't provide an integer solution.

    Here's the link to my mess

  • source
  • [–] 3 points 10 months ago (1 child)

    The graph of Day 11, created with very little effort using the dot utility of graphviz. Because of a stupid parsing error I ended up with a cyclic graph, so this visualization helped in debugging.

  • source
  • [–] 2 points 10 months ago* (last edited 10 months ago) (3 children)

    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

    (/relieving)

    I would never have solved this if it weren't for AoC being only 12 days. I don't know if that was a good thing with how much this took out of me.

    • Written in the Really Ultimate Simplex Tableau (Rust) programming language
    • Over 600 lines (so long it didn't fit without using paste)
    • Two-phase simplex fully implemented, refactored to handle inequalities as well as equalities (you need inequalities during branch-and-cut)
    • Custom Fraction struct because floating-point made me sad (just like every other time I try to use it)
    • The hackiest branch-and-cut implementation ever (really, you had to add inputs with non-integer optima just to make life harder?)
    • Hideously unreadable use of iterators
    • Probably still filled with lingering bugs
    • All of the comments telling the story of my deteriorating mental state intact
    • Part 1 included as a bonus
    • Somehow, actually reasonably performant (37.1 +/- 0.3 ms)

    Solution

  • source
  • [–] 2 points 10 months ago

    (Browser-based) Javascript

    My initial approach was to consider each junction as a circuit, and then merge the closest circuits. Took me way to long to realize the problem statement for part 1 made this unworkable (as you need to count redundant connections as "attempts"). Thankfully part 2 doesn't care about how many connections you make, so I was able to reuse that code to solve part 2.

    To solve part 1 "the right way", I first thought I had to store a circuit as a collection of pairs of junctions (literally, the collection of connections). Oh god was that messy code; 3 layers of nested for-loops and it still wasn't getting close to the answer. I eventually realized I could reference the junctions' indices in the input to simplify things, allowing me to manipulate simple sets of ints. Combine with pre-calculating the distances once before starting the connecting/merging and you end up with a surprisingly simple and snappy algorithm.

    Given I realized these optimizations after restarting part 1, my solution for part 2 doesn't take advantage of any of them and takes an eye-watering 22 seconds to terminate on average. It probably re-computes more distances than it computes new ones, for each iteration...

    Code

    function getExampleText() {
      return `162,817,812
    57,618,57
    906,360,560
    592,479,940
    352,342,300
    466,668,158
    542,29,236
    431,825,988
    739,650,466
    52,470,668
    216,146,977
    819,987,18
    117,168,530
    805,96,715
    346,949,466
    970,615,88
    941,993,340
    862,61,35
    984,92,344
    425,690,689
    `
    }
    function part1(inputText, maxConnections) {
      const junctions = inputText.trimEnd().split('\n')
        .map(line => line.split(',')
          .map(strValue => Number.parseInt(strValue, 10)));
      // compute the distance between each junction exactly once
      const annotatedDistances = Array(Math.ceil(((junctions.length - 1) ** 2) / 2));
      let counter = 0;
      for (let i = 0; i < junctions.length; i++) {
        const [xI, yI, zI] = junctions[i];
        for (let j = i + 1; j < junctions.length; j++) {
          const [xJ, yJ, zJ] = junctions[j];
          const distanceSquared = (xJ - xI) ** 2 + (yJ - yI) ** 2 + (zJ - zI) ** 2;
          annotatedDistances[counter] = [distanceSquared, i, j];
          counter++;
        }
      }
      // sort the pairs of junctions by closest to farthest
      annotatedDistances.sort(([dA], [dB]) => dA - dB);
      // connect the maxConnections-closest junctions
      const circuits = [];
      for (const [_, i, j] of annotatedDistances.slice(0, maxConnections)) {
        // find any existing circuit(s) that already contain(s) one of the junctions
        let existingCircuits = [];
        for (let circuitIndex = 0; circuitIndex < circuits.length; circuitIndex++) {
          const circuit = circuits[circuitIndex];
          if (circuit.has(i) || circuit.has(j)) {
            existingCircuits.push(circuitIndex);
          }
        }
        // 3 possible cases:
        // the junctions are not part of any existing circuits => connecting them creates a new circuit
        if (existingCircuits.length === 0) {
          circuits.push(new Set([i, j]));
        }
        // the junctions are part of 1 existing circuit => connecting them extends this existing circuit to encompass an additional junction (if only 1 junction is already in this circuit)
        else if (existingCircuits.length === 1) {
          const circuitToExtend = circuits[existingCircuits[0]];
          circuitToExtend.add(i);
          circuitToExtend.add(j);
        }
        // the junctions are part of 2 distinct existing circuits => connecting them merges these two circuits
        else if (existingCircuits.length === 2) {
          // merge circuit(s) with new connection between junctions i and j
          const circuitMergerIndex = existingCircuits.shift();
          for (const circuitToBeMergedIndex of existingCircuits) {
            circuits[circuitMergerIndex] = circuits[circuitMergerIndex].union(circuits[circuitToBeMergedIndex]);
            circuits.splice(circuitToBeMergedIndex, 1);
          }
        }
      }
      circuits.sort((a, b) => b.size - a.size);
    //console.debug({ sortedCircuits: structuredClone(circuits) });
      return circuits.slice(0, 3).reduce((accu, next) => accu * next.size, 1)
    }
    {
      const start = performance.now();
    //const result = part1(getExampleText(), 10);
      const result = part1(document.body.textContent, 1_000);
      const end = performance.now();
      console.info({
        day: 7,
        part: 1,
        time: end - start,
        result
      });
    }
    
    function part2(inputText) {
      const circuits = inputText
        .trimEnd()
        .split('\n')
        .map(line => ([
          line.split(',')
          .map(strVal => Number.parseInt(strVal, 10))
        ]))
      let junctionA, junctionB;
      while (circuits.length > 1) {
        let smallestSquaredDistance = Number.POSITIVE_INFINITY;
        let toConnectA, toConnectB;
        for (const circuitA of circuits) {
          for (const circuitB of circuits) {
            if (circuitA === circuitB) {
              continue;
            }
            let squaredCircuitDistance = Number.POSITIVE_INFINITY;
            for (const [xA, yA, zA] of circuitA) {
              for (const [xB, yB, zB] of circuitB) {
                const distance = (xB - xA) ** 2 + (yB - yA) ** 2 + (zB - zA) ** 2;
                if (distance < squaredCircuitDistance) {
                  squaredCircuitDistance = distance;
                  junctionA = [xA, yA, zA];
                  junctionB = [xB, yB, zB];
                }
              }
            }
            if (squaredCircuitDistance < smallestSquaredDistance) {
              smallestSquaredDistance = squaredCircuitDistance;
              toConnectA = circuitA;
              toConnectB = circuitB;
            }
          }
        }
        if (toConnectA.length > toConnectB.length) {
          toConnectA.push(...toConnectB);
          const bIndex = circuits.indexOf(toConnectB);
          circuits.splice(bIndex, 1);
        } else {
          toConnectB.push(...toConnectA);
          const aIndex = circuits.indexOf(toConnectA);
          circuits.splice(aIndex, 1);
        }
      }
      return junctionA[0] * junctionB[0];
    }
    {
      const start = performance.now();
    //  const result = part2(getExampleText());
      const result = part2(document.body.textContent);
      const end = performance.now();
      console.info({
        day: 7,
        part: 2,
        time: end - start,
        result
      });
    }
    

  • source
  • view more: next β€Ί