diff --git a/README.md b/README.md index 8fcdb1c..6aa92b3 100644 --- a/README.md +++ b/README.md @@ -37,4 +37,4 @@ go test ./cmd/01 | 09 | [Problem](https://adventofcode.com/2025/day/9) | [Solution](./cmd/09/) | Part 1 was such a nice break from the madness of day 8. Basically I need to go build some video games. | | 10 | [Problem](https://adventofcode.com/2025/day/10) | [Solution](./cmd/10/) | Part 1 took me way to look cause I couldn't figure out how to generate all button press combinations. | | 11 | [Problem](https://adventofcode.com/2025/day/11) | [Solution](./cmd/11/) | DFS to the rescue again. | -| 12 | [Problem](https://adventofcode.com/2025/day/12) | [Solution](./cmd/12/) | | +| 12 | [Problem](https://adventofcode.com/2025/day/12) | [Solution](./cmd/12/) | I did it...but the example input takes a long time to find solution | diff --git a/cmd/12/main.go b/cmd/12/main.go new file mode 100644 index 0000000..347f280 --- /dev/null +++ b/cmd/12/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "regexp" + "sort" + "strconv" + "strings" + + "github.com/StevanFreeborn/advent-of-code-2025/cmd/12/shape" + "github.com/StevanFreeborn/advent-of-code-2025/internal/file" +) + +const COLON = ":" +const X = "x" + +func SolvePartOne(filePath string) int { + twoNewLineRegex := regexp.MustCompile(`\r?\n\r?\n`) + newLineRegex := regexp.MustCompile(`\r?\n`) + + input := file.ReadAllText(filePath) + sections := twoNewLineRegex.Split(strings.TrimSpace(input), -1) + + total := 0 + shapes := map[int][]shape.Shape{} + regions := []string{} + + for _, section := range sections { + lines := newLineRegex.Split(strings.TrimSpace(section), -1) + header := lines[0] + + if strings.Contains(header, COLON) && strings.Contains(header, X) == false { + shape := shape.From(lines) + shapes[shape.Id()] = shape.GenerateVariants() + continue + } + + for _, line := range lines { + if strings.Contains(line, COLON) { + regions = append(regions, line) + } + } + } + + for _, region := range regions { + parts := strings.Split(region, COLON) + dims := strings.Split(parts[0], X) + width, _ := strconv.Atoi(dims[0]) + height, _ := strconv.Atoi(dims[1]) + + countsStr := strings.Fields(parts[1]) + requiredShapes := []int{} + + for id, s := range countsStr { + count, _ := strconv.Atoi(s) + + for range count { + requiredShapes = append(requiredShapes, id) + } + } + + if canFit(width, height, requiredShapes, shapes) { + total++ + } + } + + return total +} + +type item struct { + id int + area int + variants []shape.Shape +} + +func canFit(width int, height int, requiredShapes []int, shapes map[int][]shape.Shape) bool { + totalArea := 0 + + itemsToPlace := make([]item, 0, len(requiredShapes)) + + for _, id := range requiredShapes { + shapeVariants := shapes[id] + area := shapeVariants[0].Area() + totalArea += area + itemsToPlace = append(itemsToPlace, item{id: id, area: area, variants: shapeVariants}) + } + + if totalArea > width*height { + return false + } + + sort.Slice(itemsToPlace, func(i, j int) bool { + return itemsToPlace[i].area > itemsToPlace[j].area + }) + + grid := make([][]bool, height) + + for i := range grid { + grid[i] = make([]bool, width) + } + + return checkFit(0, itemsToPlace, grid, width, height) +} + +func checkFit(index int, itemsToPlace []item, grid [][]bool, width int, height int) bool { + if index == len(itemsToPlace) { + return true + } + + itemToPlace := itemsToPlace[index] + + for _, variant := range itemToPlace.variants { + maxRow := variant.MaxRow() + maxColumn := variant.MaxColumn() + + lastRow := height - maxRow + lastColumn := width - maxColumn + + for r := range lastRow { + for c := range lastColumn { + if canPlace(grid, r, c, variant) { + place(grid, r, c, variant) + + if checkFit(index+1, itemsToPlace, grid, width, height) { + return true + } + + unplace(grid, r, c, variant) + } + } + } + } + + return false +} + +func canPlace(grid [][]bool, r, c int, variant shape.Shape) bool { + for _, position := range variant.Positions() { + absoluteRow := r + position.Row() + absoluteColumn := c + position.Column() + isOccupied := grid[absoluteRow][absoluteColumn] + + if isOccupied { + return false + } + } + + return true +} + +func place(grid [][]bool, r, c int, variant shape.Shape) { + for _, position := range variant.Positions() { + absoluteRow := r + position.Row() + absoluteColumn := c + position.Column() + grid[absoluteRow][absoluteColumn] = true + } +} + +func unplace(grid [][]bool, r, c int, variant shape.Shape) { + for _, position := range variant.Positions() { + absoluteRow := r + position.Row() + absoluteColumn := c + position.Column() + grid[absoluteRow][absoluteColumn] = false + } +} diff --git a/cmd/12/main_test.go b/cmd/12/main_test.go new file mode 100644 index 0000000..750a22a --- /dev/null +++ b/cmd/12/main_test.go @@ -0,0 +1,28 @@ +package main_test + +import ( + "testing" + + solution "github.com/StevanFreeborn/advent-of-code-2025/cmd/12" +) + +// NOTE: This takes long time +func TestSolvePartOneWithExampleInput(t *testing.T) { + expected := 2 + + result := solution.SolvePartOne("EXAMPLE.txt") + + if result != expected { + t.Errorf("got %d but wanted %d", result, expected) + } +} + +func TestSolvePartOneWithInput(t *testing.T) { + expected := 550 + + result := solution.SolvePartOne("INPUT.txt") + + if result != expected { + t.Errorf("got %d but wanted %d", result, expected) + } +} diff --git a/cmd/12/shape/shape.go b/cmd/12/shape/shape.go new file mode 100644 index 0000000..1a852dd --- /dev/null +++ b/cmd/12/shape/shape.go @@ -0,0 +1,264 @@ +// Package shape provides a model and methods for representing and manipulating shapes +package shape + +import ( + "fmt" + "maps" + "slices" + "strconv" + "strings" + + "github.com/StevanFreeborn/advent-of-code-2025/internal/position" +) + +type Shape interface { + Id() int + GetKey() string + GenerateVariants() []Shape + RotateClockwise() Shape + FlipVertically() Shape + String() string + Area() int + MaxRow() int + MaxColumn() int + Positions() []position.Position +} + +type shape struct { + id int + positions []position.Position +} + +func From(lines []string) Shape { + idStr := strings.TrimSuffix(lines[0], ":") + id, _ := strconv.Atoi(idStr) + + positions := []position.Position{} + + for r, line := range lines[1:] { + for c, char := range line { + if char != '#' { + continue + } + + position := position.From(r, c) + positions = append(positions, position) + } + } + + return shape{id: id, positions: positions} +} + +func (s shape) Positions() []position.Position { + return s.positions +} + +func (s shape) MaxRow() int { + maxRow := 0 + + for _, p := range s.positions { + if p.Row() > maxRow { + maxRow = p.Row() + } + } + + return maxRow +} + +func (s shape) MaxColumn() int { + maxColumn := 0 + + for _, p := range s.positions { + if p.Column() > maxColumn { + maxColumn = p.Column() + } + } + + return maxColumn +} + +func (s shape) Area() int { + return len(s.positions) +} + +func (s shape) GenerateVariants() []Shape { + shapes := []Shape{} + var current Shape + current = s + numberOfTransformations := 4 + + for range numberOfTransformations { + shapes = append(shapes, current) + flipped := current.FlipVertically() + shapes = append(shapes, flipped) + current = current.RotateClockwise() + } + + // // Rotate 90 clockwise + // for range numberOfTransformations { + // current = current.RotateClockwise() + // shapes = append(shapes, current) + // } + // + // current = s + // + // // Flip then rotate 90 clockwise + // for range numberOfTransformations { + // shapes = append(shapes, current) + // flipped := current.FlipVertically() + // shapes = append(shapes, flipped) + // current = flipped.RotateClockwise() + // } + // + // current = s + // + // // Rotate 90 clockwise then flip + // for range numberOfTransformations { + // shapes = append(shapes, current) + // rotated := current.RotateClockwise() + // shapes = append(shapes, rotated) + // current = rotated.FlipVertically() + // } + // + // current = s + // rotated := current.RotateClockwise().RotateClockwise() + // flipped := rotated.FlipVertically() + // shapes = append(shapes, flipped) + + seenShapes := map[string]Shape{} + + for _, s := range shapes { + seenShapes[s.GetKey()] = s + } + + return slices.Collect(maps.Values(seenShapes)) +} + +func (s shape) RotateClockwise() Shape { + rotatedPositions := []position.Position{} + + for _, p := range s.positions { + rotatedPosition := position.From(p.Column(), -p.Row()) + rotatedPositions = append(rotatedPositions, rotatedPosition) + } + + rotatedShape := shape{ + id: s.id, + positions: rotatedPositions, + } + + return normalize(rotatedShape) +} + +func (s shape) FlipVertically() Shape { + flippedPositions := []position.Position{} + + for _, p := range s.positions { + flippedPosition := position.From(p.Row(), -p.Column()) + flippedPositions = append(flippedPositions, flippedPosition) + } + + flippedShape := shape{ + id: s.id, + positions: flippedPositions, + } + + return normalize(flippedShape) +} + +func (s shape) GetKey() string { + var sb strings.Builder + + for _, p := range s.positions { + fmt.Fprintf(&sb, "%d,%d|", p.Row(), p.Column()) + } + + return sb.String() +} + +func (s shape) Id() int { + return s.id +} + +func normalize(s shape) Shape { + if len(s.positions) == 0 { + return shape{ + id: s.id, + positions: s.positions, + } + } + + minRow := s.positions[0].Row() + minColumn := s.positions[0].Column() + + for _, p := range s.positions { + if p.Row() < minRow { + minRow = p.Row() + } + + if p.Column() < minColumn { + minColumn = p.Column() + } + } + + nps := []position.Position{} + + for _, p := range s.positions { + nr := p.Row() - minRow + nc := p.Column() - minColumn + np := position.From(nr, nc) + nps = append(nps, np) + } + + return shape{ + id: s.id, + positions: nps, + } +} + +func (s shape) String() string { + if len(s.positions) == 0 { + return "(EMPTY SHAPE)" + } + + maxRow := 0 + maxColumn := 0 + + for _, p := range s.positions { + if p.Row() > maxRow { + maxRow = p.Row() + } + + if p.Column() > maxColumn { + maxColumn = p.Column() + } + } + + tempGrid := make([][]string, maxRow+1) + + for r := range tempGrid { + tempGrid[r] = make([]string, maxColumn+1) + + for c := range tempGrid[r] { + tempGrid[r][c] = "." + } + } + + for _, p := range s.positions { + tempGrid[p.Row()][p.Column()] = "#" + } + + var shape strings.Builder + + for _, row := range tempGrid { + var rowString strings.Builder + + for _, str := range row { + rowString.WriteString(str) + } + + shape.WriteString(rowString.String() + "\n") + } + + return shape.String() +} diff --git a/cmd/12/shape/shape_test.go b/cmd/12/shape/shape_test.go new file mode 100644 index 0000000..722110a --- /dev/null +++ b/cmd/12/shape/shape_test.go @@ -0,0 +1,149 @@ +package shape_test + +import ( + "strings" + "testing" + + "github.com/StevanFreeborn/advent-of-code-2025/cmd/12/shape" +) + +func TestGenerateVariants(t *testing.T) { + t.Run("works for shape 0", func(t *testing.T) { + expectedVariants := [][]string{ + { + "###", + "##.", + "##.", + }, + { + "###", + ".##", + ".##", + }, + { + "###", + "###", + "#..", + }, + { + "###", + "###", + "..#", + }, + { + ".##", + ".##", + "###", + }, + { + "##.", + "##.", + "###", + }, + } + + lines := []string{ + "0:", + "###", + "##.", + "##.", + } + + shape := shape.From(lines) + + result := shape.GenerateVariants() + + for _, ev := range expectedVariants { + expected := strings.Join(ev, "\n") + + found := false + + for _, v := range result { + if strings.TrimSpace(v.String()) == expected { + found = true + } + } + + if found == false { + t.Errorf("expected to find variant but did not:\n%v", expected) + } + } + }) + + t.Run("works for shape 1", func(t *testing.T) { + expectedVariants := [][]string{ + { + "###", + "##.", + ".##", + }, + { + ".##", + "###", + "#.#", + }, + { + "##.", + ".##", + "###", + }, + { + "#.#", + "###", + "##.", + }, + { + "###", + ".##", + "##.", + }, + { + "##.", + "###", + "#.#", + }, + { + "##.", + "###", + "#.#", + }, + { + ".##", + "##.", + "###", + }, + { + "#.#", + "###", + ".##", + }, + } + + lines := []string{ + "0:", + "###", + "##.", + ".##", + } + + shape := shape.From(lines) + + result := shape.GenerateVariants() + + for _, ev := range expectedVariants { + expected := strings.Join(ev, "\n") + + found := false + + for _, v := range result { + if strings.TrimSpace(v.String()) == expected { + found = true + } + } + + if found == false { + t.Errorf("expected to find variant but did not:\n%v", expected) + } + } + }) +}