Skip to content

Commit f6a193d

Browse files
authored
Day 12 2024 (#271)
* Day 12 2024
1 parent b4e97c4 commit f6a193d

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package me.peckb.aoc._2024.calendar.day12
2+
3+
import javax.inject.Inject
4+
import me.peckb.aoc.generators.InputGenerator.InputGeneratorFactory
5+
import java.util.LinkedList
6+
import java.util.Queue
7+
8+
class Day12 @Inject constructor(
9+
private val generatorFactory: InputGeneratorFactory,
10+
) {
11+
fun partOne(filename: String) = generatorFactory.forFile(filename).read { input ->
12+
val (_, fields) = createFields(input)
13+
14+
fields.sumOf { field -> field.perimeter * field.crops.size }
15+
}
16+
17+
fun partTwo(filename: String) = generatorFactory.forFile(filename).read { input ->
18+
val (area, knownFields) = createFields(input)
19+
20+
// setup empty map
21+
val largeArea = mutableListOf<MutableList<Char>>()
22+
val height = area.size + 4
23+
val width = area[0].size + 4
24+
25+
repeat(height) { largeArea.add(mutableListOf<Char>().apply { repeat(width) { add('.') } }) }
26+
27+
knownFields.forEach { field ->
28+
field.crops.forEach { c -> largeArea[c.y + 2][c.x + 2] = c.type }
29+
field.crops.forEach { c ->
30+
if (c.onPerimeter) {
31+
val n = (c.y + 2) - 1 to (c.x + 2)
32+
val s = (c.y + 2) + 1 to (c.x + 2)
33+
val e = (c.y + 2) to (c.x + 2) + 1
34+
val w = (c.y + 2) to (c.x + 2) - 1
35+
36+
// if the given neighbor matches, put an "edge" on the marker
37+
listOf(n, s, e, w).forEach { (y, x) -> if (largeArea[y][x] != c.type) { largeArea[y][x] = '+' } }
38+
}
39+
}
40+
41+
// sweep for the sides going left to right
42+
field.doSweep(
43+
firstLoopSize = largeArea.size,
44+
secondLoopSize = largeArea[0].size,
45+
item = { y: Int, x: Int -> largeArea[y][x] },
46+
n1 = { y: Int, x: Int -> largeArea[y - 1][x] },
47+
n2 = { y: Int, x: Int -> largeArea[y + 1][x] },
48+
)
49+
50+
// sweep for the sides going top to bottom
51+
field.doSweep(
52+
firstLoopSize = largeArea[0].size,
53+
secondLoopSize = largeArea.size,
54+
item = { x: Int, y: Int -> largeArea[y][x] },
55+
n1 = { x: Int, y: Int -> largeArea[y][x - 1] },
56+
n2 = { x: Int, y: Int -> largeArea[y][x + 1] },
57+
)
58+
59+
// reset the map
60+
repeat(largeArea.size) { y -> repeat(largeArea[y].size) { x -> largeArea[y][x] = '.' } }
61+
}
62+
63+
knownFields.sumOf { field -> field.sides * field.crops.size}
64+
}
65+
66+
private fun createFields(input: Sequence<String>): Pair<MutableList<MutableList<Crop>>, MutableList<Field>> {
67+
val area = mutableListOf<MutableList<Crop>>()
68+
69+
val unKnownCrops = mutableSetOf<Crop>()
70+
val knownFields = mutableListOf<Field>()
71+
72+
input.forEachIndexed { yIndex, line ->
73+
val row = mutableListOf<Crop>()
74+
line.forEachIndexed { xIndex, char ->
75+
val crop = Crop(char, yIndex, xIndex)
76+
row.add(crop)
77+
unKnownCrops.add(crop)
78+
}
79+
area.add(row)
80+
}
81+
82+
while (unKnownCrops.isNotEmpty()) {
83+
val nextAreaNodes: Queue<Crop> = LinkedList()
84+
nextAreaNodes.add(unKnownCrops.first())
85+
86+
val field = Field()
87+
loop@ while(nextAreaNodes.isNotEmpty()) {
88+
val c = nextAreaNodes.remove()
89+
if (field.crops.contains(c)) { continue@loop }
90+
91+
unKnownCrops.remove(c)
92+
field.crops.add(c)
93+
94+
val n = area.get(c.y - 1, c.x)
95+
val s = area.get(c.y + 1, c.x)
96+
val e = area.get(c.y, c.x + 1)
97+
val w = area.get(c.y, c.x - 1)
98+
99+
listOf(n, s, e, w).forEach { neighbor: Crop? ->
100+
if (neighbor == null) {
101+
field.perimeter++
102+
c.onPerimeter = true
103+
} else {
104+
if (neighbor.type == c.type) {
105+
nextAreaNodes.add(neighbor)
106+
} else {
107+
field.perimeter++
108+
c.onPerimeter = true
109+
}
110+
}
111+
}
112+
}
113+
114+
knownFields.add(field)
115+
}
116+
117+
return area to knownFields
118+
}
119+
120+
private fun Field.doSweep(
121+
firstLoopSize: Int,
122+
secondLoopSize: Int,
123+
item: (Int, Int) -> Char,
124+
n1: (Int, Int) -> Char,
125+
n2: (Int, Int) -> Char,
126+
) {
127+
val fieldType = crops.first().type
128+
129+
// loop starting in one direction
130+
repeat(firstLoopSize) { f ->
131+
var n1SideFound = false
132+
var n2SideFound = false
133+
// for that direction, iterate through the other
134+
repeat(secondLoopSize) { s ->
135+
// if we found an "edge"
136+
if (item(f, s) == '+') {
137+
// check the first neighbor (ignoring the direction of travel)
138+
val n1Char = n1(f, s)
139+
if (n1Char == fieldType) {
140+
// if the neighbor matches our type, this is a side, track it if we weren't
141+
if (!n1SideFound) {
142+
n1SideFound = true
143+
}
144+
} else if (n1SideFound) {
145+
// if the neighbor wasn't our type; but we were tracking, we found the full side
146+
n1SideFound = false
147+
sides++
148+
}
149+
// check the second neighbor (ignoring the direction of travel)
150+
val n2Char = n2(f, s)
151+
if (n2Char == fieldType) {
152+
// if the neighbor matches our type, this is a side, track it if we weren't
153+
if (!n2SideFound) {
154+
n2SideFound = true
155+
}
156+
} else if (n2SideFound) {
157+
// if the neighbor wasn't our type; but we were tracking, we found the full side
158+
n2SideFound = false
159+
sides++
160+
}
161+
} else {
162+
if (n1SideFound) {
163+
// if the item wasn't a wall, and we were tracking, we found the full side
164+
n1SideFound = false
165+
sides++
166+
}
167+
if (n2SideFound) {
168+
// if the item wasn't a wall, and we were tracking, we found the full side
169+
n2SideFound = false
170+
sides++
171+
}
172+
}
173+
}
174+
}
175+
}
176+
}
177+
178+
private fun MutableList<MutableList<Crop>>.get(y: Int, x: Int): Crop? {
179+
return if (y in indices && x in this[y].indices) { this[y][x] } else { null }
180+
}
181+
182+
data class Crop(val type: Char, val y: Int, val x: Int) {
183+
var onPerimeter = false
184+
}
185+
186+
data class Field(val crops: MutableSet<Crop> = mutableSetOf(), var perimeter: Long = 0L) {
187+
var sides = 0L
188+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
## [Day 12: Garden Groups](https://adventofcode.com/2024/day/12)

src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import me.peckb.aoc._2024.calendar.day08.Day08Test
1111
import me.peckb.aoc._2024.calendar.day09.Day09Test
1212
import me.peckb.aoc._2024.calendar.day10.Day10Test
1313
import me.peckb.aoc._2024.calendar.day11.Day11Test
14+
import me.peckb.aoc._2024.calendar.day12.Day12Test
1415
import javax.inject.Singleton
1516
import me.peckb.aoc.DayComponent
1617
import me.peckb.aoc.InputModule
@@ -30,4 +31,5 @@ internal interface TestDayComponent : DayComponent {
3031
fun inject(day09Test: Day09Test)
3132
fun inject(day10Test: Day10Test)
3233
fun inject(day11Test: Day11Test)
34+
fun inject(day12Test: Day12Test)
3335
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package me.peckb.aoc._2024.calendar.day12
2+
3+
import javax.inject.Inject
4+
5+
import me.peckb.aoc._2024.DaggerTestDayComponent
6+
import org.junit.jupiter.api.Assertions.assertEquals
7+
import org.junit.jupiter.api.BeforeEach
8+
import org.junit.jupiter.api.Test
9+
10+
internal class Day12Test {
11+
@Inject
12+
lateinit var day12: Day12
13+
14+
@BeforeEach
15+
fun setup() {
16+
DaggerTestDayComponent.create().inject(this)
17+
}
18+
19+
@Test
20+
fun testDay12PartOne() {
21+
assertEquals(1446042, day12.partOne(DAY_12))
22+
}
23+
24+
@Test
25+
fun testDay12PartTwo() {
26+
assertEquals(902742, day12.partTwo(DAY_12))
27+
}
28+
29+
companion object {
30+
private const val DAY_12: String = "advent-of-code-input/2024/day12.input"
31+
}
32+
}

0 commit comments

Comments
 (0)