package daytwelve import ( "os" "sort" "strconv" "strings" "advent-of-code/internal/registry" ) func init() { registry.Register("2025D12", ParseInput, PartOne, PartTwo) } func ParseInput(filepath string) []string { content, _ := os.ReadFile(filepath) return strings.Split(string(content), "\n") } const ( exactAreaThreshold = 140 exactPiecesThreshold = 10 exactMinSideThreshold = 8 ) type point struct { x, y int } type shapeOrientation struct { cells []point width int height int } type shape struct { area int orientations []shapeOrientation } type region struct { width, height int counts []int } func parsePuzzle(data []string) ([]shape, []region) { lines := compactLines(data) shapeBlocks := make(map[int][]string) shapeOrder := make([]int, 0) regions := make([]region, 0) for idx := 0; idx < len(lines); { line := lines[idx] if width, height, counts, ok := parseRegionLine(line); ok { regions = append(regions, region{width: width, height: height, counts: counts}) idx++ continue } shapeIdx, ok := parseShapeHeader(line) if !ok { idx++ continue } idx++ rows := make([]string, 0, 4) for idx < len(lines) { if _, _, _, isRegion := parseRegionLine(lines[idx]); isRegion { break } if _, isHeader := parseShapeHeader(lines[idx]); isHeader { break } rows = append(rows, lines[idx]) idx++ } if _, exists := shapeBlocks[shapeIdx]; !exists { shapeOrder = append(shapeOrder, shapeIdx) } shapeBlocks[shapeIdx] = rows } sort.Ints(shapeOrder) shapes := make([]shape, 0, len(shapeOrder)) for _, shapeIdx := range shapeOrder { cells := make([]point, 0) for y, row := range shapeBlocks[shapeIdx] { for x, char := range row { if char == '#' { cells = append(cells, point{x: x, y: y}) } } } shapes = append(shapes, shape{ area: len(cells), orientations: buildOrientations(cells), }) } for idx := range regions { regions[idx].counts = normalizeCounts(regions[idx].counts, len(shapes)) } return shapes, regions } func buildOrientations(cells []point) []shapeOrientation { transform := func(p point, t int) point { switch t { case 0: return point{x: p.x, y: p.y} case 1: return point{x: p.x, y: -p.y} case 2: return point{x: -p.x, y: p.y} case 3: return point{x: -p.x, y: -p.y} case 4: return point{x: p.y, y: p.x} case 5: return point{x: p.y, y: -p.x} case 6: return point{x: -p.y, y: p.x} default: return point{x: -p.y, y: -p.x} } } result := make([]shapeOrientation, 0, 8) seen := make(map[string]bool) if len(cells) == 0 { return result } for t := range 8 { transformed := make([]point, len(cells)) minX, minY := cells[0].x, cells[0].y for idx, cell := range cells { p := transform(cell, t) transformed[idx] = p if p.x < minX { minX = p.x } if p.y < minY { minY = p.y } } maxX, maxY := 0, 0 for idx := range transformed { transformed[idx].x -= minX transformed[idx].y -= minY if transformed[idx].x > maxX { maxX = transformed[idx].x } if transformed[idx].y > maxY { maxY = transformed[idx].y } } sort.Slice(transformed, func(i, j int) bool { if transformed[i].y == transformed[j].y { return transformed[i].x < transformed[j].x } return transformed[i].y < transformed[j].y }) var builder strings.Builder for _, p := range transformed { builder.WriteString(strconv.Itoa(p.x)) builder.WriteByte(',') builder.WriteString(strconv.Itoa(p.y)) builder.WriteByte(';') } key := builder.String() if seen[key] { continue } seen[key] = true result = append(result, shapeOrientation{ cells: transformed, width: maxX + 1, height: maxY + 1, }) } return result } type placement struct { mask []uint64 } func buildPlacements(orientation shapeOrientation, width, height int) []placement { maxX := width - orientation.width maxY := height - orientation.height if maxX < 0 || maxY < 0 { return nil } cells := width * height words := (cells + 63) / 64 placements := make([]placement, 0, (maxX+1)*(maxY+1)) for y := 0; y <= maxY; y++ { for x := 0; x <= maxX; x++ { mask := make([]uint64, words) for _, cell := range orientation.cells { idx := (y+cell.y)*width + (x + cell.x) mask[idx/64] |= 1 << (idx % 64) } placements = append(placements, placement{mask: mask}) } } return placements } func canFitExactly(shapes []shape, r region) bool { totalPieces, totalArea, valid := regionStats(shapes, r.counts) if !valid { return false } boardArea := r.width * r.height if totalArea > boardArea { return false } if totalPieces == 0 { return true } byTypePlacements := make([][]placement, len(shapes)) for t, s := range shapes { var placements []placement for _, o := range s.orientations { placements = append(placements, buildPlacements(o, r.width, r.height)...) } byTypePlacements[t] = placements if r.counts[t] > 0 && len(placements) == 0 { return false } } indices := make([]int, 0, totalPieces) for t, count := range r.counts { for range count { indices = append(indices, t) } } sort.Slice(indices, func(i, j int) bool { a, b := indices[i], indices[j] if shapes[a].area == shapes[b].area { return len(byTypePlacements[a]) < len(byTypePlacements[b]) } return shapes[a].area > shapes[b].area }) occupancy := make([]uint64, ((boardArea + 63) / 64)) usedArea := 0 var canPlace func(mask []uint64) bool canPlace = func(mask []uint64) bool { for idx := range mask { if occupancy[idx]&mask[idx] != 0 { return false } } return true } place := func(mask []uint64) { for idx := range mask { occupancy[idx] |= mask[idx] } } remove := func(mask []uint64) { for idx := range mask { occupancy[idx] &^= mask[idx] } } var dfs func(level int) bool dfs = func(level int) bool { if level == len(indices) { return true } freeCells := boardArea - usedArea remainingArea := totalArea - usedArea if remainingArea > freeCells { return false } t := indices[level] for _, p := range byTypePlacements[t] { if !canPlace(p.mask) { continue } place(p.mask) usedArea += shapes[t].area if dfs(level + 1) { return true } usedArea -= shapes[t].area remove(p.mask) } return false } return dfs(0) } func canFitRegion(shapes []shape, r region) bool { totalPieces, totalArea, valid := regionStats(shapes, r.counts) if !valid { return false } if totalArea > r.width*r.height { return false } if totalPieces == 0 { return true } if r.width*r.height <= exactAreaThreshold || totalPieces <= exactPiecesThreshold || min(r.width, r.height) <= exactMinSideThreshold { return canFitExactly(shapes, r) } return true } func compactLines(data []string) []string { lines := make([]string, 0, len(data)) for _, line := range data { trimmed := strings.TrimSpace(line) if trimmed != "" { lines = append(lines, trimmed) } } return lines } func parseShapeHeader(line string) (int, bool) { if !strings.HasSuffix(line, ":") { return 0, false } value := strings.TrimSuffix(line, ":") index, err := strconv.Atoi(value) if err != nil { return 0, false } return index, true } func parseRegionLine(line string) (int, int, []int, bool) { dimPart, countsPart, ok := strings.Cut(line, ":") if !ok { return 0, 0, nil, false } width, height, ok := parseDimensions(strings.TrimSpace(dimPart)) if !ok { return 0, 0, nil, false } counts := parseCounts(countsPart) return width, height, counts, true } func parseDimensions(value string) (int, int, bool) { left, right, ok := strings.Cut(value, "x") if !ok { return 0, 0, false } width, err := strconv.Atoi(left) if err != nil { return 0, 0, false } height, err := strconv.Atoi(right) if err != nil { return 0, 0, false } return width, height, true } func parseCounts(value string) []int { fields := strings.Fields(value) counts := make([]int, 0, len(fields)) for _, field := range fields { count, err := strconv.Atoi(field) if err != nil { continue } counts = append(counts, count) } return counts } func normalizeCounts(counts []int, size int) []int { if len(counts) == size { return counts } if len(counts) > size { return counts[:size] } normalized := make([]int, size) copy(normalized, counts) return normalized } func regionStats(shapes []shape, counts []int) (totalPieces int, totalArea int, valid bool) { for idx, count := range counts { if count < 0 { return 0, 0, false } if idx >= len(shapes) { if count > 0 { return 0, 0, false } continue } totalPieces += count totalArea += count * shapes[idx].area } return totalPieces, totalArea, true } func PartOne(data []string) int { shapes, regions := parsePuzzle(data) fit := 0 for _, region := range regions { if canFitRegion(shapes, region) { fit++ } } return fit } func PartTwo(data []string) int { return 0 }