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/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{
|
||||
|
|
|
|||
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