feat: completed day 12 for advent of code

This commit is contained in:
Ceferino Patino 2025-12-21 18:35:41 -06:00
commit fb45088fdd
Signed by: c4patino
SSH key fingerprint: SHA256:9fQ9TsujGrdNNi76mnsu63v7dS5JOmHRZEqBOl49OR8
4 changed files with 375 additions and 0 deletions

View file

@ -20,6 +20,7 @@ import (
"cpatino.com/advent-of-code/2025/day09" "cpatino.com/advent-of-code/2025/day09"
"cpatino.com/advent-of-code/2025/day10" "cpatino.com/advent-of-code/2025/day10"
"cpatino.com/advent-of-code/2025/day11" "cpatino.com/advent-of-code/2025/day11"
"cpatino.com/advent-of-code/2025/day12"
) )
var days = map[int]func(string) (any, any){ var days = map[int]func(string) (any, any){
@ -34,6 +35,7 @@ var days = map[int]func(string) (any, any){
9: day09.Run, 9: day09.Run,
10: day10.Run, 10: day10.Run,
11: day11.Run, 11: day11.Run,
12: day12.Run,
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{

306
2025/day12/day12.go Executable file
View file

@ -0,0 +1,306 @@
package day12
import (
"bufio"
"os"
"strconv"
"strings"
)
type Shape struct {
cells [][2]int
}
type Region struct {
w, h int
counts []int
}
func parseInput(filename string) ([]Shape, []Region) {
file, err := os.Open(filename)
if err != nil {
panic(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
shapes := []Shape{}
regions := []Region{}
readingShapes := true
current := [][]rune{}
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if readingShapes {
if strings.HasSuffix(line, ":") {
if len(current) > 0 {
shapes = append(shapes, makeShape(current))
current = [][]rune{}
}
continue
}
if line[0] == '#' || line[0] == '.' {
current = append(current, []rune(line))
continue
}
if len(current) > 0 {
shapes = append(shapes, makeShape(current))
current = [][]rune{}
}
readingShapes = false
}
if !readingShapes {
parts := strings.Split(line, ":")
if len(parts) != 2 {
panic("invalid region line")
}
wh := strings.TrimSpace(parts[0])
countsStr := strings.Fields(strings.TrimSpace(parts[1]))
whp := strings.Split(wh, "x")
if len(whp) != 2 {
panic("invalid region size")
}
w, _ := strconv.Atoi(whp[0])
h, _ := strconv.Atoi(whp[1])
arr := make([]int, len(countsStr))
for i := range countsStr {
v, _ := strconv.Atoi(countsStr[i])
arr[i] = v
}
regions = append(regions, Region{w: w, h: h, counts: arr})
}
}
if err := scanner.Err(); err != nil {
panic(err)
}
if len(current) > 0 {
shapes = append(shapes, makeShape(current))
}
return shapes, regions
}
func makeShape(grid [][]rune) Shape {
cells := [][2]int{}
for y, row := range grid {
for x, ch := range row {
if ch == '#' {
cells = append(cells, [2]int{x, y})
}
}
}
minx, miny := 1<<30, 1<<30
for _, c := range cells {
if c[0] < minx {
minx = c[0]
}
if c[1] < miny {
miny = c[1]
}
}
for i := range cells {
cells[i][0] -= minx
cells[i][1] -= miny
}
return Shape{cells: cells}
}
func transforms(s Shape) []Shape {
uniq := map[string]Shape{}
for r := 0; r < 4; r++ {
rot := rotate(s, r)
for _, f := range []bool{false, true} {
var t Shape
if f {
// horizontal flip
cells := make([][2]int, len(rot.cells))
copy(cells, rot.cells)
for i := range cells {
cells[i][0] = -cells[i][0]
}
t = Shape{cells: cells}
} else {
t = rot
}
minx, miny := 1<<30, 1<<30
for _, c := range t.cells {
if c[0] < minx {
minx = c[0]
}
if c[1] < miny {
miny = c[1]
}
}
for i := range t.cells {
t.cells[i][0] -= minx
t.cells[i][1] -= miny
}
// sort cells for stable key
cells := make([][2]int, len(t.cells))
copy(cells, t.cells)
for i := 0; i < len(cells); i++ {
for j := i + 1; j < len(cells); j++ {
if cells[j][1] < cells[i][1] || (cells[j][1] == cells[i][1] && cells[j][0] < cells[i][0]) {
cells[i], cells[j] = cells[j], cells[i]
}
}
}
key := keyOf(Shape{cells: cells})
uniq[key] = Shape{cells: cells}
}
}
out := make([]Shape, 0, len(uniq))
for _, v := range uniq {
out = append(out, v)
}
return out
}
func rotate(s Shape, times int) Shape {
cells := make([][2]int, len(s.cells))
copy(cells, s.cells)
for t := 0; t < times; t++ {
for i := range cells {
x, y := cells[i][0], cells[i][1]
// 90° clockwise
cells[i] = [2]int{y, -x}
}
}
return Shape{cells: cells}
}
func keyOf(s Shape) string {
b := strings.Builder{}
for _, c := range s.cells {
b.WriteString(strconv.Itoa(c[0]))
b.WriteByte(',')
b.WriteString(strconv.Itoa(c[1]))
b.WriteByte(';')
}
return b.String()
}
func canFitRegion(w, h int, shapes []Shape, counts []int) bool {
transByIdx := make([][]Shape, len(shapes))
for i := range shapes {
transByIdx[i] = transforms(shapes[i])
}
// quick area check
area := w * h
required := 0
sizes := make([]int, len(shapes))
for i := range shapes {
sz := len(shapes[i].cells)
sizes[i] = sz
}
for i, c := range counts {
required += sizes[i] * c
}
if required > area {
return false
}
totalPieces := 0
order := []int{}
for i, c := range counts {
for k := 0; k < c; k++ {
order = append(order, i)
}
totalPieces += c
}
// sort by piece area descending
for i := 0; i < len(order); i++ {
for j := i + 1; j < len(order); j++ {
if sizes[order[j]] > sizes[order[i]] {
order[i], order[j] = order[j], order[i]
}
}
}
grid := make([][]bool, h)
for y := 0; y < h; y++ {
grid[y] = make([]bool, w)
}
var place func(idx int) bool
place = func(idx int) bool {
if idx == totalPieces {
return true
}
shapeIdx := order[idx]
for _, t := range transByIdx[shapeIdx] {
// compute bounds of transform
maxx, maxy := 0, 0
for _, c := range t.cells {
if c[0] > maxx {
maxx = c[0]
}
if c[1] > maxy {
maxy = c[1]
}
}
for oy := 0; oy+maxy < h; oy++ {
for ox := 0; ox+maxx < w; ox++ {
// try place
ok := true
for _, c := range t.cells {
x := ox + c[0]
y := oy + c[1]
if grid[y][x] {
ok = false
break
}
}
if !ok {
continue
}
for _, c := range t.cells {
x := ox + c[0]
y := oy + c[1]
grid[y][x] = true
}
if place(idx + 1) {
return true
}
for _, c := range t.cells {
x := ox + c[0]
y := oy + c[1]
grid[y][x] = false
}
}
}
}
return false
}
return place(0)
}
func Part1(shapes []Shape, regions []Region) int {
count := 0
for _, r := range regions {
if canFitRegion(r.w, r.h, shapes, r.counts) {
count++
}
}
return count
}
func Part2() int { return -1 }
func Run(filename string) (any, any) {
shapes, regions := parseInput(filename)
part1 := Part1(shapes, regions)
part2 := Part2()
return part1, part2
}

34
2025/day12/day12_test.go Executable file
View file

@ -0,0 +1,34 @@
package day12
import (
"os"
"testing"
)
type TestCase struct {
FileName string
Part1 any
Part2 any
}
func TestDay12(t *testing.T) {
tests := []TestCase{
{"test.txt", 2, nil},
{"input.txt", 469, nil},
}
for _, test := range tests {
if _, err := os.Stat(test.FileName); os.IsNotExist(err) {
t.Fatalf("test file does not exist: %v", test.FileName)
}
part1, part2 := Run(test.FileName)
if test.Part1 != nil && part1 != test.Part1 {
t.Fatalf("%s: unexpected part1:\nwant:\t%v\ngot:\t%v", test.FileName, test.Part1, part1)
}
if test.Part2 != nil && part2 != test.Part2 {
t.Fatalf("%s: unexpected part2:\nwant:\t%v\ngot:\t%v", test.FileName, test.Part2, part2)
}
}
}

33
2025/day12/test.txt Executable file
View file

@ -0,0 +1,33 @@
0:
###
##.
##.
1:
###
##.
.##
2:
.##
###
##.
3:
##.
###
##.
4:
###
#..
###
5:
###
.#.
###
4x4: 0 0 0 0 2 0
12x5: 1 0 1 0 2 2
12x5: 1 0 1 0 3 2