From fb45088fddf80916ee0d5e136d0574b846ed08d1 Mon Sep 17 00:00:00 2001 From: C4 Patino Date: Sun, 21 Dec 2025 18:35:41 -0600 Subject: [PATCH] feat: completed day 12 for advent of code --- 2025/cmd/root.go | 2 + 2025/day12/day12.go | 306 +++++++++++++++++++++++++++++++++++++++ 2025/day12/day12_test.go | 34 +++++ 2025/day12/test.txt | 33 +++++ 4 files changed, 375 insertions(+) create mode 100755 2025/day12/day12.go create mode 100755 2025/day12/day12_test.go create mode 100755 2025/day12/test.txt diff --git a/2025/cmd/root.go b/2025/cmd/root.go index 8f1156e..bd67c1c 100644 --- a/2025/cmd/root.go +++ b/2025/cmd/root.go @@ -20,6 +20,7 @@ import ( "cpatino.com/advent-of-code/2025/day09" "cpatino.com/advent-of-code/2025/day10" "cpatino.com/advent-of-code/2025/day11" + "cpatino.com/advent-of-code/2025/day12" ) var days = map[int]func(string) (any, any){ @@ -34,6 +35,7 @@ var days = map[int]func(string) (any, any){ 9: day09.Run, 10: day10.Run, 11: day11.Run, + 12: day12.Run, } var rootCmd = &cobra.Command{ diff --git a/2025/day12/day12.go b/2025/day12/day12.go new file mode 100755 index 0000000..d9c1c35 --- /dev/null +++ b/2025/day12/day12.go @@ -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 +} diff --git a/2025/day12/day12_test.go b/2025/day12/day12_test.go new file mode 100755 index 0000000..02aeaf7 --- /dev/null +++ b/2025/day12/day12_test.go @@ -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) + } + } +} diff --git a/2025/day12/test.txt b/2025/day12/test.txt new file mode 100755 index 0000000..e5e1b3d --- /dev/null +++ b/2025/day12/test.txt @@ -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