diff --git a/internal/2025/DayTwelve/code.go b/internal/2025/DayTwelve/code.go index 7539957..1290268 100644 --- a/internal/2025/DayTwelve/code.go +++ b/internal/2025/DayTwelve/code.go @@ -2,6 +2,8 @@ package daytwelve import ( "os" + "sort" + "strconv" "strings" "advent-of-code/internal/registry" @@ -15,3 +17,444 @@ 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 +}