advent-of-code/2025/day12/day12.go

306 lines
5.8 KiB
Go
Executable file

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
}