It’s not always easy to distinguish between existentialism and a bad mood.

  • 12 Posts
  • 210 Comments
Joined 1 year ago
cake
Cake day: July 2nd, 2023

help-circle


  • 16 commentary

    DFS (it’s all dfs all the time now, this is my life now, thanks AOC) pruned by unless-I-ever-passed-through-here-with-a-smaller-score-before worked well enough for Pt1. In Pt2 in order to get all the paths I only had to loosen the filter by a) not pruning for equal scores and b) only prune if the direction also matched.

    Pt2 was easier for me because while at first it took me a bit to land on lifting stuff from Djikstra’s algo to solve the challenge maze before the sun turns supernova, as I tend to store the paths for debugging anyway it was trivial to group them by score and count by distinct tiles.




  • Pt2 commentary

    I randomly got it by sorting for the most robots in the bottom left quadrant while looking for robot concentrations, it was number 13. Despite being in the centre of the grid it didn’t show up when sorting for most robots in the middle 30% columns of the screen, which is kind of wicked, in the traditional sense.

    The first things I tried was looking for horizontal symmetry (find a grid where all the lines have the same number of robots on the left and on the right of the middle axis, there is none, and the tree is about a third to a quarted of the matrix on each side) and looking for grids where the number of robots increased towards the bottom of the image (didn’t work, because turns out tree is in the middle of the screen).

    I thinks I was on the right track with looking for concentrations of robots, wish I’d thought about ranking the matrices according to the amount of robots lined up without gaps. Don’t know about minimizing the safety score, sorting according to that didn’t show the tree anywhere near the first tens.

    Realizing that the patterns start recycling at ~10.000 iterations simplified things considerably.

    The tree on the terminal output

    (This is three matrices separated by rows of underscores)


  • 13 commentary

    Solved p1 by graph search before looking a bit closer on the examples and going, oh…

    In pt2 I had some floating point weirdness when solving for keypress count, I was checking if the key presses where integers (can’t press button A five and half times after all) by checking if A = floor(A) and sometimes A would drop to the number below when floored, i.e. it was in reality (A-1).999999999999999999999999999999999999999999999. Whatever, I rounded it away but I did spend a stupid amount of time on it because it didn’t happen in the example set.






  • 11 discussion with spoliers

    Well my pt1 solution would require something like at least 1.5 petabytes RAM to hold the fully expanded array, so it was back to the drawing board for pt2 😁

    Luckily I noticed the numbers produced in every iteration were incredibly repetitive, so I assigned a separate accumulator to each one, and every iteration I only kept the unique numbers and updated the corresponding accumulators with how many times they had appeared, and finally I summed the accumulators.

    The most unique numbers in one iteration were 3777, the 75 step execution was basically instant.

    edit: other unhinged attempts included building a cache with how many pebbles resulted from a number after x steps that I would start using after reaching the halfway point, so every time I found a cached number I would replace that branch with the final count according to the remaining steps, but I couldn’t think of a way to actually track how many pebbles result downstream from a specific pebble, but at least it got me thinking about tracking something along each pebble.

    11 code
    // F# as usual
    // fst and snd are tuple deconstruction helpers
    
    [<TailCall>]
    let rec blink (idx:int) (maxIdx:int) (pebbles : (int64*int64) list) =
        if idx = maxIdx
        then pebbles |> List.sumBy snd
        else
            pebbles
            // Expand array
            |> List.collect (fun (pebbleId, pebbleCount) -> 
                let fpb = float pebbleId
                let digitCount = Math.Ceiling(Math.Log(fpb + 1.0,10))      
                match pebbleId with
                | 0L -> [ 1L, pebbleCount ]
                | x when digitCount % 2.0 = 0.0 -> 
                    let factor = Math.Pow(10,digitCount/2.0)
                    let right = fpb % factor
                    let left = (fpb - right) / factor
                    [int64 left, pebbleCount; int64 right,pebbleCount]   
                | x -> [ x * 2024L, pebbleCount ])
            // Compress array
            |> List.groupBy fst
            |> List.map (fun (pebbleId, pebbleGroup) -> pebbleId, pebbleGroup |> List.sumBy snd)
            |> blink (idx+1) maxIdx
    
    
    "./input.example"
    |> Common.parse
    |> List.map (fun pebble -> pebble,1L)
    |> blink 0 25 
    |> Global.shouldBe 55312L
    
    "./input.actual"
    |> Common.parse
    |> List.map (fun pebble -> pebble,1L)
    |> blink 0 75 
    |> printfn "Pebble count after 75 blinks is %d" 
    



  • 10 commentary

    Yeah basically if you were doing DFS and forgot to check if you’d already visited the next node you were solving for pt2, since the rule about the next node always having a value of +1 compared to the current one was already preventing cyclic paths.

    10 Code

    Hardly a groundbreaking implementation but I hadn’t posted actual code in a while so

    (* F# - file reading code and other boilerplate omited *)
    
    let mapAllTrails (matrix : int array2d) =
        let rowCount = matrix |> Array2D.length1
        let colCount = matrix |> Array2D.length2
    
        let rec search (current:int*int) (visited: HashSet<int*int>) (path: (int*int) list) : (int*int) list list= 
            let (row,col) = current
            let currentValue = matrix.[row,col]
    
            // Remove to solve for 10-2
            visited.Add (row,col) |> ignore
    
            // If on a 9 return the complete path
            if currentValue = 9 then [List.append path [row,col] ]
            // Otherwise filter for eligible neihboring cells and continue search
            else                    
                [ row-1, col;row, col-1; row, col+1; row+1,col]
                |> List.filter (fun (r,c) -> 
                    not (visited.Contains(r,c))
                    && r >= 0 && c>=0 && r < rowCount && c < colCount
                    && matrix.[r,c]-currentValue = 1 )
                |> List.collect (fun next ->
                    [row,col] 
                    |> List.append path  
                    |> search next visited)
    
        // Find starting cells, i.e. contain 0
        matrix
        |> Global.matrixIndices
        |> Seq.filter (fun (row,col) -> matrix.[row,col] = 0)
        // Find all trails starting from those cells and flatten the result
        |> Seq.collect (fun trailhead -> search trailhead (HashSet<int*int>()) [])
        
    
    "./input.example"
    |> Common.parse 
    |> mapAllTrails
    |> Seq.length
    |> Global.shouldBe 81
    
    "./input.actual"
    |> Common.parse 
    |> mapAllTrails
    |> Seq.length
    |> printfn "The sum total of trail rankings is %d"
    



  • My graph search solves 7-1 and passes the example cases for 7-2, but gives too low a result for the complete puzzle input, and there’s no way I’m manually going through every case to find the false negative. On to day 8 I guess.

    7-2 Check case by simple graph search that mostly works
    // F#
    let isLegit ((total: int64), (calibration : int64 array)) = 
    
        let rec search (index : int) (acc: int64) =
            let currentValue = calibration.[index]
            
            [Add; Times; Concat] // operators - remove 'Concat' to solve for 7-1
            |> List.exists (fun op -> // List.exists returns true the first time the lambda returns true, so search stops at first true
                    match op with // update accumulator
                    | Add -> acc + currentValue
                    | Times -> acc * currentValue
                    | Concat -> int64 (sprintf "%d%d" acc currentValue)
                    |> function // stop search on current accumulator value (state) exceeding total, or being just right
                    | state when state > total -> false
                    | state when state = total && index < (calibration.Length-1) -> false // this was the problem
                    | state when state = total && index = (calibration.Length-1) -> true
                    | state -> // stop if index exceeds input length, or continue search
                        if index+1 = calibration.Length
                        then false
                        else search (index+1) state
            )
         
        // start search from second element using the first as current sum
        search 1 calibration.[0]
    

    EDIT: total && index < (calibration.Length-1) -> false – i.e. stop if you reach the total before using all numbers, well, also stops you from checking the next operator, So, removing it worked.

    Rubber ducking innocent people on the internets works, who knew.


  • tl;dr: Day 5 was most perfectly fine code thrown out for me, because I ran face first into eliminating imaginary edge cases instead of starting out simple.

    5-1 commentary

    I went straight into a rabbit hole of doing graph traversal to find all implicit rules (i.e. 1|2, 2|3, 3|4 imply 1|3, 1|4, 2|4) so I could validate updates by making sure all consequent pairs appear in the expanded ruleset. Basically I would depth first search a tree with page numbers for nodes and rules for edges, to get all branches and recombine them to get the full ruleset.

    So ideally 1|2, 2|3, 3|4 -> 1|2|3|4 -> 1|2, 2|3, 3|4, 1|3, 1|4, 2|4

    Except I forgot the last part and just returned the branch elements pairwise in sequence, which is just the original rules, which I noticed accidentally after the fact since I was getting correct results, because apparently it was completely unnecessary and I was just overthinking myself into several corners at the same time.

    5-2 commentary and some code

    The obvious cornerstone was the comparison function to reorder the invalid updates, this is what I came up with:

    let comparerFactory (ruleset: (int*int) list) :int -> int -> int = 
        let leftIndex = 
            ruleset 
            |> List.groupBy fst 
            |> List.map (fun (key,grp)-> key, grp |> List.map snd)
            |> Map.ofList
    
        fun page1 page2 -> 
            match (leftIndex  |> Map.tryFind page1) with
            | Some afterSet when afterSet |> List.contains page2 -> -1
            | _ -> 1
    

    The memoization pattern is for caching an index of rules grouped by the before page, so I can sort according to where each part of the comparison appears. I started out with having a second index where the key was the ‘after’ page of the rule which I would check if the page didn’t appear on the left side of any rule, but it turned out I could just return the opposite case, so again unnecessary.


  • discussion

    Same, except in 4-1 I used a recursive function to traverse each direction according to the offset decided by the selected direction (like SW is row++,col–) , due to functional programming induced brain damage.

    Would have been pretty useful too if 4-2 turned out to be about finding longer patterns, instead of smaller and in half the directions.

    4-1

    Inlined some stuff to fit everything in one function:

    let rec isXmas (row:int) (col:int) (dir:int) (depth:int) (arr: char array2d) : bool =
        let value = arr.[row,col]
        if depth = 3
        then value = 'S'
        else if  [|'X';'M';'A';'S'|].[depth] = value
        then
            let (nextRow, nextCol) =
                match dir with
                | 1 -> row + 1, col - 1
                | 2 -> row + 1, col
                | 3 -> row + 1, col + 1
                | 4 -> row, col - 1
                | 6 -> row, col + 1
                | 7 -> row - 1, col - 1
                | 8 -> row - 1, col
                | 9 -> row - 1, col + 1
                | _ -> failwith $"{dir} isn't a numpad direction." 
    
            let rowCount = arr |> Array2D.length1
            let colCount = arr |> Array2D.length2
           
            if nextRow >= 0 && nextRow < rowCount && nextCol >= 0 && nextCol < colCount
            then isXmas nextRow nextCol dir (depth+1) arr
            else false
        else false
    

    Then you run this for every appropriately pruned ‘X’ times every direction and count the trues.