feat: completed day 12 for advent of code
This commit is contained in:
parent
9cf9674e7f
commit
fb45088fdd
4 changed files with 375 additions and 0 deletions
|
|
@ -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
306
2025/day12/day12.go
Executable 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
34
2025/day12/day12_test.go
Executable 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
33
2025/day12/test.txt
Executable 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue