Skip to content

Day 12. Garden Groups

We need to help gardener on calculating the price for the garden plots fencing. Each plot has only one plant (letter). Regions are formed by plots of the same type that touch either horizontally or vertically.

This is again a grid related problem.

Part One

In this part we calculate the price as Area * Perimeter. The area is the number of plants in the plot. And the perimeter is the number of "borders". Each plot has a border if it touches the plot of another type or the edge of the grid.

If the task would not include condition that one region can have enclave of another region, we could just use Pick's theorem to calculate the area and perimeter.

With the enclave condition we can iterate over the grid and for each unvisited plot we start the flood fill algorithm to find the region and it's properties. During the region search we mark the visited plots to avoid checking them on the main loop.

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
type GardenMap struct {
    Map [][]rune
}

type ParsedInput = GardenMap

...

func PartOne(inp ParsedInput) int {
    total := 0
    grid, err := NewGridFromSlices(inp.Map)
    if err != nil {
        panic(fmt.Sprintf("creating grid %s", err))
    }

    visited := NewGridWithDefault(grid.Rows, grid.Columns, false)

    for pi, point := range grid.Cells {
        if visited.Cells[pi].Value {
            continue
        }

        area, perimeter := findFullPlot(grid, visited, point)
        total += area * perimeter
    }

    return total
}
const cellSides = 4

// area, perimeter
func findFullPlot(grid *Grid[rune], globalVisited *Grid[bool], start Point[rune]) (int, int) {
    queue := make([]Point[rune], 0, 16)
    queue = append(queue, start)

    area, perimeter := 0, 0

    for len(queue) > 0 {
        point := queue[0]
        queue = queue[1:]
        if globalVisited.Get(point.Row, point.Col).Value {
            continue
        }

        globalVisited.Set(point.Row, point.Col, true)
        area++

        nCode, polyNeighbors := encodePointNeighbors(grid, point)
        cellPerimiter := calcPerimeter(nCode)
        perimeter += cellPerimiter

        for _, neighbor := range polyNeighbors {
            queue = append(queue, neighbor)
        }
    }

    return area, perimeter
}

I've done each plot perimeter calculation a bit complicated way by encoding the cell neighbors and checking the bit mask for the borders. But this will be useful for the next part of the task:

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
func encodePointNeighbors(grid *Grid[rune], point Point[rune]) (int, []Point[rune]) {
    // 0 1 2
    // 3 X 4
    // 5 6 7
    row, col := point.Row, point.Col
    moves := []GridMove{
        MoveUpLeft, MoveUp, MoveUpRight,
        MoveLeft, MoveRight,
        MoveDownLeft, MoveDown, MoveDownRight,
    }

    name := point.Value

    codedNeighbors := 0
    crossNeighbors := make([]Point[rune], 0, 4)

    for i, move := range moves {
        nPoint := grid.GetAnyByMove(row, col, move)
        if nPoint.Value != name {
            codedNeighbors |= 1 << i
            continue
        }
        if move == MoveUp || move == MoveRight || move == MoveDown || move == MoveLeft {
            crossNeighbors = append(crossNeighbors, nPoint)
        }
    }

    return codedNeighbors, crossNeighbors
}

// Each cell has 1 bit around
// 0 1 2
// 3 X 4
// 5 6 7
// So we can use bit mask to find the borders by setting
// the bits for each neighbor: 0 for the same name, 1 for different
func prepareByte(powers []int) int {
    mask := 0
    for _, power := range powers {
        if power < 0 || power > 7 {
            panic(fmt.Sprintf("invalid power %d", power))
        }
        mask |= 1 << power
    }
    return mask
}

var (
    // 2, 16, 64, 8
    upBorderMask    int = prepareByte([]int{1})
    rightBorderMask int = prepareByte([]int{4})
    downBorderMask  int = prepareByte([]int{6})
    leftBorderMask  int = prepareByte([]int{3})
)

func calcPerimeter(nCode int) int {
    perimeter := 0
    for _, mask := range borderMasks {
        if nCode&mask == mask {
            perimeter++
        }
    }
    return perimeter
}

Part Two

The formula has changed to Area * Sides. Each straight fence line is the side. This adds some complexity. But we can use the fact that polygon side count is equal to the number of corners because each angle represents a change in direction of the fence. And again enclaves complicate the task because we need to track inner corners as well. So we check the triplets of the cell neighbors and match them to the possible corner types by the bit mask:

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    upLeftMask    int = prepareByte([]int{0, 1, 3})
    upRightMask   int = prepareByte([]int{1, 2, 4})
    downRightMask int = prepareByte([]int{4, 6, 7})
    downLeftMask  int = prepareByte([]int{3, 5, 6})

    // 1 0 .
    // 0 X .
    upLeftInner int = prepareByte([]int{0})
    // 1 1 .
    // 1 X .
    upLeftOuter int = prepareByte([]int{0, 1, 3})
    // . 1 .
    // 1 X .
    upLeftInside int = prepareByte([]int{1, 3})

    // . 0 1
    // . X 0
    upRightInner int = prepareByte([]int{2})
    // . 1 1
    // . X 1
    upRightOuter int = prepareByte([]int{1, 2, 4})
    // . 1 .
    // . X 1
    upRightInside int = prepareByte([]int{1, 4})

    // . X 0
    // . 0 1
    downRightInner int = prepareByte([]int{7})
    // . X 1
    // . 1 1
    downRightOuter int = prepareByte([]int{4, 6, 7})
    // . X 1
    // . 1 .
    downRightInside int = prepareByte([]int{4, 6})

    // 0 X .
    // 1 0 .
    downLeftInner int = prepareByte([]int{5})
    // 1 X .
    // 1 1 .
    downLeftOuter int = prepareByte([]int{3, 5, 6})
    // 1 X .
    // . 1 .
    downLeftInside int = prepareByte([]int{3, 6})
)

func findAngles(nCode int) int {
    angles := 0
    upLeft := nCode & upLeftMask
    upRight := nCode & upRightMask
    downRight := nCode & downRightMask
    downLeft := nCode & downLeftMask

    if upLeft == upLeftOuter || upLeft == upLeftInner || upLeft == upLeftInside {
        angles++
    }

    if upRight == upRightOuter || upRight == upRightInner || upRight == upRightInside {
        angles++
    }

    if downRight == downRightOuter || downRight == downRightInner || downRight == downRightInside {
        angles++
    }

    if downLeft == downLeftOuter || downLeft == downLeftInner || downLeft == downLeftInside {
        angles++
    }

    return angles
}

The rest of the code is similar to the first part. We just need to replace the perimeter calculation with the angles calculation:

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
func PartTwo(inp ParsedInput) int {
    total := 0
    grid, err := NewGridFromSlices(inp.Map)
    if err != nil {
        panic(fmt.Sprintf("creating grid %s", err))
    }

    visited := NewGridWithDefault(grid.Rows, grid.Columns, false)

    for pi, point := range grid.Cells {
        if visited.Cells[pi].Value {
            continue
        }
        // We can calculate sides by finding angles count
        area, sides := findFullPlotWithSides(grid, visited, point)
        total += area * sides

    }

    return total
}

// area, sides
func findFullPlotWithSides(
    grid *Grid[rune],
    globalVisited *Grid[bool],
    start Point[rune],
) (int, int) {
    queue := make([]Point[rune], 0, 16)
    queue = append(queue, start)

    area := 0
    angles := 0

    for len(queue) > 0 {
        point := queue[0]
        queue = queue[1:]

        if globalVisited.Get(point.Row, point.Col).Value {
            continue
        }
        globalVisited.Set(point.Row, point.Col, true)
        area++

        nCode, polyNeighbors := encodePointNeighbors(grid, point)
        // how many sides our polygon has can be calculated by the number of angles it has
        pAngles := findAngles(nCode)
        angles += pAngles

        for _, neighbor := range polyNeighbors {
            queue = append(queue, neighbor)
        }
    }

    return area, angles
}

Tags

  • grid
  • flood fill
  • bitmasks