From 36679efb837ca2bd4277c22954734bfdddc8cb25 Mon Sep 17 00:00:00 2001 From: alexchao26 Date: Thu, 15 Aug 2024 12:53:28 -0400 Subject: [PATCH] 2023-22 straightforward grid problem but plenty of bugs to workout --- 2023/day22/main.go | 223 ++++++++++++++++++++++++++++++++++++++++ 2023/day22/main_test.go | 65 ++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 2023/day22/main.go create mode 100644 2023/day22/main_test.go diff --git a/2023/day22/main.go b/2023/day22/main.go new file mode 100644 index 0000000..58e1b77 --- /dev/null +++ b/2023/day22/main.go @@ -0,0 +1,223 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/algos" + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + bricks := parseInput(input) + + dropBricks(bricks) + + // basically have a graph... + // if a brick supports nothing, it can be removed + // if a brick supports + removableBricks := map[int]bool{} + for _, brick := range bricks { + if len(brick.supports) == 0 { + // supports nothing + removableBricks[brick.index] = true + } else { + // check ALL supported bricks, if this is the ONLY support for those + // then this brick CANNOT be removed + hasUniqueDependency := false + for supportedBrickIndex := range brick.supports { + if len(bricks[supportedBrickIndex].supportedBy) == 1 { + hasUniqueDependency = true + } + } + if !hasUniqueDependency { + removableBricks[brick.index] = true + } + } + } + + return len(removableBricks) +} + +func dropBricks(bricks []*brick) { + // process bricks by lowest z values + sort.Slice(bricks, func(i, j int) bool { + return bricks[i].start[2] < bricks[j].start[2] + }) + + // re-index, pesky bug... + for i, brick := range bricks { + brick.index = i + } + + // all bricks are < 10 units of volume, so can store their coords in a map... + // also all bricks in the input are straight lines, only one dimension will be > 1 + occupiedCells := map[[3]int]int{} + for _, brick := range bricks { + + isBlocked := false + for brick.start[2] > 1 && !isBlocked { + for _, coord := range brick.coords { + downOne := [3]int{coord[0], coord[1], coord[2] - 1} + if index, ok := occupiedCells[downOne]; ok { + isBlocked = true + + brick.supportedBy[index] = true + bricks[index].supports[brick.index] = true + } + } + + if !isBlocked { + for i := range brick.coords { + brick.coords[i][2]-- + } + brick.start[2]-- + brick.end[2]-- + } + } + + for _, coord := range brick.coords { + occupiedCells[coord] = brick.index + } + } +} + +func part2(input string) int { + bricks := parseInput(input) + dropBricks(bricks) + + total := 0 + + // chain reaction + for i := range bricks { + bricksCopy := copyAllBricks(bricks) + startingBrick := bricksCopy[i] + + queueToRemove := []*brick{} + for in := range startingBrick.supports { + delete(bricksCopy[in].supportedBy, startingBrick.index) + queueToRemove = append(queueToRemove, bricksCopy[in]) + } + + removed := 0 + for len(queueToRemove) > 0 { + br := queueToRemove[0] + queueToRemove = queueToRemove[1:] + + if len(br.supportedBy) > 0 { + continue + } + + removed++ + + // check every brick it supports, remove self from it's supportedBy map + // then add to queue to be checked + for supportedBrickIndex := range br.supports { + delete(bricksCopy[supportedBrickIndex].supportedBy, br.index) + if len(bricksCopy[supportedBrickIndex].supportedBy) == 0 { + queueToRemove = append(queueToRemove, bricksCopy[supportedBrickIndex]) + } + } + } + + total += removed + } + + return total +} + +func copyAllBricks(bricks []*brick) []*brick { + copiedBricks := []*brick{} + for _, b := range bricks { + newBrick := &brick{ + // start: []int{}, + // end: []int{}, + index: b.index, + // coords: [][3]int{}, + supportedBy: map[int]bool{}, + supports: map[int]bool{}, + } + // need full copies of these otherwise they'll point to the same underlying maps + for k, v := range b.supportedBy { + newBrick.supportedBy[k] = v + } + for k, v := range b.supports { + newBrick.supports[k] = v + } + copiedBricks = append(copiedBricks, newBrick) + } + return copiedBricks +} + +type brick struct { + start, end []int + index int + coords [][3]int + supportedBy map[int]bool + supports map[int]bool +} + +func parseInput(input string) (ans []*brick) { + for _, line := range strings.Split(input, "\n") { + coords := [6]int{} + for i, part := range algos.SplitStringOn(line, []string{",", "~"}) { + coords[i] = cast.ToInt(part) + } + + if coords[0] > coords[3] || coords[1] > coords[4] || coords[2] > coords[5] { + panic("unordered input") + } + + allCoords := [][3]int{} + for x := coords[0]; x <= coords[3]; x++ { + for y := coords[1]; y <= coords[4]; y++ { + for z := coords[2]; z <= coords[5]; z++ { + allCoords = append(allCoords, [3]int{x, y, z}) + } + } + } + + ans = append(ans, &brick{ + start: coords[:3], + end: coords[3:], + index: len(ans), + coords: allCoords, + supportedBy: map[int]bool{}, + supports: map[int]bool{}, + }) + } + + return ans +} diff --git a/2023/day22/main_test.go b/2023/day22/main_test.go new file mode 100644 index 0000000..8afd636 --- /dev/null +++ b/2023/day22/main_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "testing" +) + +var example = `1,0,1~1,2,1 +0,0,2~2,0,2 +0,2,3~2,2,3 +0,0,4~0,2,4 +2,0,5~2,2,5 +0,1,6~2,1,6 +1,1,8~1,1,9` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 5, + }, + { + name: "actual", + input: input, + want: 471, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 7, + }, + { + name: "actual", + input: input, + want: 68525, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +}