306 lines
5.8 KiB
Go
Executable file
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
|
|
}
|