this post was submitted on 12 Dec 2024
15 points (89.5% liked)

Advent Of Code

920 readers
1 users here now

An unofficial home for the advent of code community on programming.dev!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

AoC 2024

Solution Threads

M T W T F S S
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 1 year ago
MODERATORS
 

Day 12: Garden Groups

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

you are viewing a single comment's thread
view the rest of the comments
[โ€“] [email protected] 2 points 3 weeks ago* (last edited 3 weeks ago) (1 children)

I know I'm late, but it's still fun and I'm sure no-one will see this.

Part 2 took me way too long to get right. I was initially only returning the relative point to which a plot needed a fence. I ran into issues of knowing if it was a valid fence or not by my method of counting (later). I eventually went with returning a tuple of the plot and an enum flag of the sides that it has fences on.

For counting I grouped the points by one axis then sorted on the other and counted the number of times the transition between two wasn't contiguous.

It could by done in parallel, but the original traversal would need de-duping, which I didn't feel like doing. After that things are done on a region basis, which could be parallel.

I also can't help but notice mine is by far the longest ( > . < )

F#

Tap for spoiler

type Plant = char
type Plot = Plant * Point2I
type Region = Plot list

let area region = List.length region

let perimeter (region:Region) (farm:Grid<Plant>) =
    (0, region)
    ||> List.fold (fun sum plot ->
        movements4 (snd plot) farm
        |> List.fold (fun acc po ->
            acc + match po with
                  | ValueNone -> 1
                  | ValueSome pother ->
                      get pother farm
                      |> fun x ->
                          match x with
                          | ValueSome v when v <> fst plot -> 1
                          | _ -> 0
            ) 0
        |> (fun x -> x + sum)
        )

let movements (plot:Plot) g (visited:HashSet<Point2I>) =
    let plant, point = plot
    movements4 point g
    |> List.choosev (fun px ->
        let vx = get px g |> ifNoneFail "couldn't get point"
        struct(px,vx))
    |> List.filter (fun plotx ->
        let struct(px, vx) = plotx
        vx = plant && not (visited.Contains px))

// visited is needed because I'm using similar logic to the trails, but no stopping point, so I
// need to make sure that it doesn't retrace itself
let rec traverse grid plot (visited:HashSet<Point2I>) =
    let plant, point = plot
    if visited.Contains(point) then []
    else
        visited.Add(point) |> ignore
        let path =
            movements plot grid visited
            |> List.filter (fun struct(newPoint, newPlant) -> newPlant = plant && visited.Contains(newPoint) |> not )
            |> List.map(fun struct(newPoint, newPlant) -> traverse grid (newPlant, newPoint) visited)
            |> List.concat
            |> List.distinct
        plot :: path

/// Returns the list of plots walked and a new grid with the traversed plots set to '.'
let walk (plot:Plot) (farm:Grid<Plant>) : Region * Grid<Plant> =
    let foundMovements = HashSet()
    let region:Region = traverse farm plot foundMovements
    let updatedGrid =
        let arr = Array2D.copy farm
        for _, p in region do
            set p arr '.' |> ignore
        arr
    (region, updatedGrid)
    
let rec findRegions (farm:Grid<Plant>) : Region list=
    farm
    |> List.unfold (fun arr ->
        match (findFirst (fun struct(_,_,v) -> v <> '.' )) arr with
        | Some value ->
            let struct(x,y,v) = value
            let point = {X = x; Y = y}
            let plot : Plot = (v, point)
            let region, newFarm = walk plot arr
            Some (region, newFarm)
        | None -> None
    )
    
let part1 input =
    (readGrid2D input (fun _ c -> c))
    |> fun grid ->
        findRegions grid
        |> List.sumBy (fun region ->
            let a = area region
            let p = perimeter region grid
            a * p)

// Part 2 ---------------------------------------------
[<Flags>]
type Side = None = 0 | Top = 1 | Right = 2| Left = 4 | Bottom = 8
type PlotWithFence = Plot * Side 

type SideAndNextPoint = Side * Point2I

// I couldn't use my original movement function because it filters out off-grid positions, so I modified it here
let getSides plot grid =
    let toPlot (side:Side) (voption:Point2I voption) =
        match voption with | ValueNone -> None, side | ValueSome point -> (Some point, side)
    [
        l (snd plot) |> toPlot Side.Left
        r (snd plot) grid |> toPlot Side.Right
        u (snd plot) |> toPlot Side.Top
        d (snd plot) grid |> toPlot Side.Bottom
    ]


let rec findEdges grid (plot:Plot) (visited:HashSet<Point2I>) =
    
    // this attempt that works is to attach the side information to each point
    // I didn't see a great way to use the existing movements function, so I copied it and added a mapping
    // so that I know in what direction the movement options are
    let sidesWithFence = getSides plot grid

    let plant, point = plot
    if visited.Add(point) |> not then Side.None
    else
        // use the movements to find edges
        let edges = 
            sidesWithFence
            |> List.fold (fun sides next ->
                match next with
                | None, side ->  sides ||| side
                | Some point, side when (get point grid >>= fun v -> v <> plant) |> ifNone false -> sides ||| side
                | _ -> sides
            ) Side.None
            
        edges

let getSideCount2 (plotFences:(Point2I * Side) list) =
    // ok, now I have information on each SIDE that a fence is one... omg I suck at this.
    // I'll try to do the whole compare horizontally and vertically thing again, but this time
    // within each I'll have to check the top and bottom, or I'll iterate it twice
    
    let getX (pf:Point2I * Side) : int = (fst pf) |> _.X
    let getY (pf:Point2I * Side) : int = (fst pf) |> _.Y
    
    let countContiguous side group sort =
        plotFences
        |> Seq.filter (fun pf -> (snd pf) &&& side = side)
        |> Seq.groupBy group 
        |> Seq.map(fun (key, g) -> g |> Seq.sortBy sort)
        // |> Array.ofSeq // debugging code
        |> Seq.sumBy(fun grouping ->
            let total =
                grouping
                |> Seq.splitByComparison (fun last current -> 
// splitByComparison is an implementation I took from ChatGPT for a function that I commonly use in C# as part of the MoreLinq library that does the same thing. Here, I'm splitting whenever there is a difference. 
// in hindsight, it probably could have been a window
                    let l = sort last 
                    let r = sort current
                    l <> (r - 1)
                )
                |> Seq.length
            total)
    
    let topCounts = countContiguous Side.Top getY getX
    let bottomCounts = countContiguous Side.Bottom getY getX
    let leftCounts = countContiguous Side.Left getX getY
    let rightCounts = countContiguous Side.Right getX getY
    
    topCounts + bottomCounts + leftCounts + rightCounts


let part2 input =
    (readGrid2D input (fun _ c -> c))
    |> fun grid ->
        findRegions grid
        // |> List.take 1
        |> List.sumBy(fun region ->
            let (plotCount, edgecount) =
                let visited = HashSet()
                let regionCounts =
                    region
                    |> List.map(fun p -> (snd p), findEdges grid p visited)
                    |> getSideCount2
                (visited.Count, regionCounts)
            
            let area = plotCount
            area * edgecount
            )

[โ€“] [email protected] 2 points 2 weeks ago (1 children)

I saw it :)

If I understand your approach for pt2, you are getting all the fences and then grouping the connected ones? That definitely seems like a harder approach. I went with the counting corner method, which was also hard, but less iterating required.

Keep the solutions coming, even as the sub wanes in activity, I still appreciate them :)

[โ€“] [email protected] 2 points 2 weeks ago

hey thanks!

I didn't check any other solutions before finishing (currently wondering way day 13 is too low), but I thought that trying to traverse fences would be a pain and since I have everything separated by regions and not traversing the array, counting corners never came to mind.

But the thought that I had was that for each region, all points will be a straight line in the V or H orientations, so if I can go up and down and count when last != next - 1, then that'll tell me that that is a contiguous piece of fence.

The idea isn't too hard, for tracking the XAxis it's

region.GroupBy(YAxis) // row
.Select(group => 
    group.Sort(g => g.XAxis) // column
        .Window(a,b => a != b - 1 ? 1 : 0).Sum()
.Sum()

Except that I used a different splitting method and that came to me later.