17

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
[-] Pyro@programming.dev 2 points 1 week ago* (last edited 1 week ago)

Python

Part 1: Simple DFS counting up the cells and exposed edges

Part 2: Still DFS, however I chose to keep track of all segments of the area, merging them if two segments connected. In the end, number of non-overlapping, non-intersecting segments is equal to number of sides. Not the most efficient solution, but it works.

import os
from collections import defaultdict

# paths
here = os.path.dirname(os.path.abspath(__file__))
filepath = os.path.join(here, "input.txt")

# read input
with open(filepath, mode="r", encoding="utf8") as f:
    data = f.read()
# setup input vars
garden = data.splitlines()
m, n = len(garden), len(garden[0])


def part1():
    visited = set()

    def calcFenceCostFrom(i, j):
        """Calculates the fencing cost of the region starting from coords (i, j)"""
        global garden, m, n

        plant_type = garden[i][j]
        stack = [(i, j)]
        area, perimeter = 0, 0

        while stack:
            ci, cj = stack.pop()
            if (ci, cj) in visited:
                continue
            visited.add((ci, cj))

            # add cell to area
            area += 1

            # check top cell
            if ci > 0 and garden[ci - 1][cj] == plant_type:
                stack.append((ci - 1, cj))
            else:
                # if no top cell, add the edge to perimeter
                perimeter += 1

            # check left cell
            if cj > 0 and garden[ci][cj - 1] == plant_type:
                stack.append((ci, cj - 1))
            else:
                # if no left cell, add the edge to perimeter
                perimeter += 1

            # check bottom cell
            if ci < m - 1 and garden[ci + 1][cj] == plant_type:
                stack.append((ci + 1, cj))
            else:
                # if no bottom cell, add the edge to perimeter
                perimeter += 1

            # check right cell
            if cj < n - 1 and garden[ci][cj + 1] == plant_type:
                stack.append((ci, cj + 1))
            else:
                # if no right cell, add the edge to perimeter
                perimeter += 1

        return area * perimeter

    # calculate fencing cost for every region
    fencing_cost = 0
    for i in range(m):
        for j in range(n):
            if (i, j) in visited:
                continue
            fencing_cost += calcFenceCostFrom(i, j)

    print(fencing_cost)


def part2():
    visited = set()

    def calcFenceCostFrom(i, j):
        """Calculates the fencing cost of the region starting from coords (i, j)"""
        global garden, m, n

        plant_type = garden[i][j]
        stack = [(i, j)]
        area = 0

        # keep track of all distinct, non-intersecting horizontal and vertical segments
        segments = {
            "H": defaultdict(set),
            "V": defaultdict(set)
        }  # fmt: skip

        while stack:
            ci, cj = stack.pop()
            if (ci, cj) in visited:
                continue
            visited.add((ci, cj))

            # add cell to area
            area += 1

            # check top cell
            if ci > 0 and garden[ci - 1][cj] == plant_type:
                stack.append((ci - 1, cj))
            else:
                # record edge segment
                ei = ci - 0.25  # push out the horizontal segment
                segment_set = segments["H"][ei]
                ej_from, ej_to = cj - 0.5, cj + 0.5  # extend the segment to connect with neighbors
                merge_segments(segment_set, ej_from, ej_to)  # merge with current segment set

            # check left cell
            if cj > 0 and garden[ci][cj - 1] == plant_type:
                stack.append((ci, cj - 1))
            else:
                # record edge segment
                ej = cj - 0.25  # push out the vertical segment
                segment_set = segments["V"][ej]
                ei_from, ei_to = ci - 0.5, ci + 0.5  # extend the segment to connect with neighbors
                merge_segments(segment_set, ei_from, ei_to)  # merge with current segment set

            # check bottom cell
            if ci < m - 1 and garden[ci + 1][cj] == plant_type:
                stack.append((ci + 1, cj))
            else:
                # record edge segment
                ei = ci + 0.25  # push out the horizontal segment
                segment_set = segments["H"][ei]
                ej_from, ej_to = cj - 0.5, cj + 0.5  # extend the segment to connect with neighbors
                merge_segments(segment_set, ej_from, ej_to)  # merge with current segment set

            # check right cell
            if cj < n - 1 and garden[ci][cj + 1] == plant_type:
                stack.append((ci, cj + 1))
            else:
                # record edge segment
                ej = cj + 0.25  # push out the vertical segment
                segment_set = segments["V"][ej]
                ei_from, ei_to = ci - 0.5, ci + 0.5  # extend the segment to connect with neighbors
                merge_segments(segment_set, ei_from, ei_to)  # merge with current segment set

        # number of distinct segments == number of sides
        sides = sum(len(segment_set) for seg_dict in segments.values() for segment_set in seg_dict.values())
        return area * sides

    def merge_segments(segment_set: set, idx_from: int, idx_to: int):
        """Merge segment into existing segment set"""
        # find any overlapping / intersecting segments before and after current
        former_segment, latter_segment = None, None
        for s_from, s_to in segment_set:
            if s_from < idx_from and s_to >= idx_from:
                former_segment = (s_from, s_to)
            if s_to > idx_to and s_from <= idx_to:
                latter_segment = (s_from, s_to)

        if former_segment is None and latter_segment is None:
            # there is no overlapping segment
            segment_set.add((idx_from, idx_to))
        elif former_segment == latter_segment:
            # the overlap segment contains our full segment
            pass
        elif former_segment is None:
            # there is a latter segment only
            segment_set.remove(latter_segment)
            segment_set.add((idx_from, latter_segment[1]))
        elif latter_segment is None:
            # there is a former segment only
            segment_set.remove(former_segment)
            segment_set.add((former_segment[0], idx_to))
        else:
            # both are disconnected segments
            segment_set.remove(former_segment)
            segment_set.remove(latter_segment)
            segment_set.add((former_segment[0], latter_segment[1]))

    fencing_cost = 0
    for i in range(m):
        for j in range(n):
            if (i, j) in visited:
                continue
            fencing_cost += calcFenceCostFrom(i, j)

    print(fencing_cost)


part1()
part2()

this post was submitted on 12 Dec 2024
17 points (94.7% liked)

Advent Of Code

920 readers
25 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