351 lines
8.1 KiB
Go
351 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"cmp"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Coordinate struct {
|
|
X int
|
|
Y int
|
|
Z int
|
|
}
|
|
|
|
type Brick []Coordinate
|
|
|
|
// BrickGraph is a map from indexes in a brick array to the dependent indexes
|
|
type BrickGraph map[int][]int
|
|
|
|
func (graph BrickGraph) ReachableFrom(idx int) map[int]struct{} {
|
|
return graph.ReachableFromExcluding(idx, nil)
|
|
}
|
|
|
|
// ReachableFromExcluding will finds all nodes reachable from the given node index, but will not explore neighbors
|
|
// in the "excluding" set.
|
|
func (graph BrickGraph) ReachableFromExcluding(idx int, excluding map[int]struct{}) map[int]struct{} {
|
|
visited := map[int]struct{}{}
|
|
toVisit := []int{idx}
|
|
for len(toVisit) > 0 {
|
|
visiting := toVisit[0]
|
|
toVisit = toVisit[1:]
|
|
for _, neighbor := range graph[visiting] {
|
|
if _, ok := visited[neighbor]; ok {
|
|
continue
|
|
} else if _, ok := excluding[neighbor]; ok {
|
|
continue
|
|
}
|
|
|
|
visited[neighbor] = struct{}{}
|
|
toVisit = append(toVisit, neighbor)
|
|
}
|
|
}
|
|
|
|
return visited
|
|
}
|
|
|
|
func (b Brick) LowestPoint() Coordinate {
|
|
minZFunc := func(a, b Coordinate) int {
|
|
return cmp.Compare(a.Z, b.Z)
|
|
}
|
|
|
|
return slices.MinFunc(b, minZFunc)
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) != 2 {
|
|
fmt.Fprintf(os.Stderr, "Usage: %s inputfile\n", os.Args[0])
|
|
os.Exit(1)
|
|
}
|
|
|
|
inputFilename := os.Args[1]
|
|
inputFile, err := os.Open(inputFilename)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not open input file: %s", err))
|
|
}
|
|
|
|
defer inputFile.Close()
|
|
|
|
inputBytes, err := io.ReadAll(inputFile)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not read input file: %s", err))
|
|
}
|
|
|
|
input := strings.TrimSpace(string(inputBytes))
|
|
inputLines := strings.Split(input, "\n")
|
|
bricks, err := parseBricks(inputLines)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("invalid input: %s", err))
|
|
}
|
|
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to parse input: %s", err))
|
|
}
|
|
|
|
fmt.Printf("Part 1: %d\n", part1(bricks))
|
|
fmt.Printf("Part 2: %d\n", part2(bricks))
|
|
}
|
|
|
|
func part1(inputBricks []Brick) int {
|
|
slammedBricks := settleBricks(inputBricks)
|
|
removable := removableBricks(slammedBricks)
|
|
|
|
return len(removable)
|
|
}
|
|
|
|
func part2(inputBricks []Brick) int {
|
|
slammedBricks := settleBricks(inputBricks)
|
|
total := 0
|
|
for i := range slammedBricks {
|
|
total += numBricksFallingByRemoval(slammedBricks, i)
|
|
}
|
|
|
|
return total
|
|
}
|
|
|
|
func settleBricks(bricks []Brick) []Brick {
|
|
sorted := slices.Clone(bricks)
|
|
sortByHeight(sorted)
|
|
|
|
slammedBricks := slices.Clone(sorted)
|
|
for i := range sorted {
|
|
brick, err := moveBrickDown(slammedBricks, i)
|
|
if err != nil {
|
|
// can't happen with our bounds
|
|
panic(err)
|
|
}
|
|
|
|
slammedBricks[i] = brick
|
|
}
|
|
|
|
return slammedBricks
|
|
}
|
|
|
|
func moveBrickDown(bricks []Brick, brickIdx int) (Brick, error) {
|
|
if brickIdx < 0 || brickIdx >= len(bricks) {
|
|
return nil, fmt.Errorf("invalid brick index %d", brickIdx)
|
|
}
|
|
|
|
occupied := occupiedPositions(bricks)
|
|
|
|
brick := slices.Clone(bricks[brickIdx])
|
|
for brick.LowestPoint().Z > 1 {
|
|
nextBrick := slices.Clone(brick)
|
|
for i, pos := range nextBrick {
|
|
nextBrick[i] = Coordinate{X: pos.X, Y: pos.Y, Z: pos.Z - 1}
|
|
if idx, ok := occupied[nextBrick[i]]; ok && idx != brickIdx {
|
|
return brick, nil
|
|
}
|
|
}
|
|
|
|
brick = nextBrick
|
|
}
|
|
|
|
return brick, nil
|
|
}
|
|
|
|
func buildBrickGraph(bricks []Brick) (incoming, outgoing BrickGraph) {
|
|
occupied := occupiedPositions(bricks)
|
|
outgoing = make(BrickGraph)
|
|
incoming = make(BrickGraph)
|
|
|
|
for i, brick := range bricks {
|
|
neighboring := map[int]struct{}{}
|
|
for _, block := range brick {
|
|
above := Coordinate{X: block.X, Y: block.Y, Z: block.Z + 1}
|
|
if occupiedBy, ok := occupied[above]; ok && occupiedBy != i {
|
|
neighboring[occupiedBy] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for neighbor := range neighboring {
|
|
outgoing[i] = append(outgoing[i], neighbor)
|
|
incoming[neighbor] = append(incoming[neighbor], i)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func removableBricks(allBricks []Brick) []int {
|
|
incoming, outgoing := buildBrickGraph(allBricks)
|
|
|
|
removable := []int{}
|
|
for i := range allBricks {
|
|
dependents := outgoing[i]
|
|
allDependentsSafe := true
|
|
for _, dependent := range dependents {
|
|
// There is more than one item which has this dependent as a dependent, so removing i would
|
|
// not allow this to fall
|
|
if len(incoming[dependent]) <= 1 {
|
|
allDependentsSafe = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if allDependentsSafe {
|
|
removable = append(removable, i)
|
|
}
|
|
|
|
}
|
|
|
|
return removable
|
|
}
|
|
|
|
func numBricksFallingByRemoval(allBricks []Brick, removeBrick int) int {
|
|
if removeBrick >= len(allBricks) {
|
|
panic("cannot remove brick not in bricks list")
|
|
}
|
|
|
|
incoming, outgoing := buildBrickGraph(allBricks)
|
|
stableNodes := map[int]struct{}{}
|
|
lastFalling := map[int]struct{}{}
|
|
for {
|
|
reachableNodes := outgoing.ReachableFromExcluding(removeBrick, stableNodes)
|
|
for reachable := range reachableNodes {
|
|
for _, parentOfReachable := range incoming[reachable] {
|
|
if _, ok := reachableNodes[parentOfReachable]; !ok && parentOfReachable != removeBrick {
|
|
// If any reachable node is accessible from another subgraph, it is "stable"
|
|
stableNodes[reachable] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
falling := outgoing.ReachableFromExcluding(removeBrick, stableNodes)
|
|
if mapKeysEqual(falling, lastFalling) {
|
|
return len(lastFalling)
|
|
}
|
|
|
|
lastFalling = falling
|
|
|
|
// We must repeat this process until we reach a state where no more stable nodes are found
|
|
// There are some cases where a node might be stable, but the children of said stable node
|
|
// must also be considered invalidated (think of it as second-order stability)
|
|
}
|
|
}
|
|
|
|
func sortByHeight(bricks []Brick) {
|
|
slices.SortFunc(bricks, func(brick1, brick2 Brick) int {
|
|
min1Z := brick1.LowestPoint()
|
|
min2Z := brick2.LowestPoint()
|
|
|
|
return cmp.Compare(min1Z.Z, min2Z.Z)
|
|
})
|
|
}
|
|
|
|
func occupiedPositions(bricks []Brick) map[Coordinate]int {
|
|
occupied := map[Coordinate]int{}
|
|
for i, brick := range bricks {
|
|
for _, pos := range brick {
|
|
occupied[pos] = i
|
|
}
|
|
}
|
|
|
|
return occupied
|
|
}
|
|
|
|
func parseBricks(inputLines []string) ([]Brick, error) {
|
|
bricks, err := tryParse(inputLines, parseBrick)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse brick: %s", err)
|
|
}
|
|
|
|
positions := map[Coordinate]struct{}{}
|
|
for _, brick := range bricks {
|
|
for _, pos := range brick {
|
|
if _, ok := positions[pos]; ok {
|
|
return nil, fmt.Errorf("bricks overlap at %+v", pos)
|
|
}
|
|
|
|
positions[pos] = struct{}{}
|
|
}
|
|
}
|
|
|
|
return bricks, nil
|
|
}
|
|
|
|
func parseBrick(line string) (Brick, error) {
|
|
pattern := regexp.MustCompile(`^(\d+),(\d+),(\d+)~(\d+),(\d+),(\d+)$`)
|
|
matches := pattern.FindStringSubmatch(line)
|
|
if matches == nil {
|
|
return nil, errors.New("malformed brick spec")
|
|
}
|
|
|
|
coordSlice1, err := tryParse([]string{matches[1], matches[2], matches[3]}, strconv.Atoi)
|
|
if err != nil {
|
|
// Can't happen, by the pattern
|
|
panic(fmt.Sprintf("could not convert coordinate to integers: %s", err))
|
|
}
|
|
|
|
coordSlice2, err := tryParse([]string{matches[4], matches[5], matches[6]}, strconv.Atoi)
|
|
if err != nil {
|
|
// Can't happen, by the pattern
|
|
panic(fmt.Sprintf("could not convert coordinate to integers: %s", err))
|
|
}
|
|
|
|
numDifferent := countDifferent(coordSlice1, coordSlice2)
|
|
if numDifferent > 1 {
|
|
return nil, fmt.Errorf("only one axis may differ in coordinates, found %d", numDifferent)
|
|
}
|
|
|
|
brick := make(Brick, 0, 3)
|
|
for x := coordSlice1[0]; x <= coordSlice2[0]; x++ {
|
|
for y := coordSlice1[1]; y <= coordSlice2[1]; y++ {
|
|
for z := coordSlice1[2]; z <= coordSlice2[2]; z++ {
|
|
brick = append(brick, Coordinate{X: x, Y: y, Z: z})
|
|
}
|
|
}
|
|
}
|
|
|
|
return brick, nil
|
|
}
|
|
|
|
func countDifferent[T comparable, S ~[]T](s1, s2 S) int {
|
|
if len(s1) != len(s2) {
|
|
panic("cannot compare lists of different lengths")
|
|
}
|
|
|
|
count := 0
|
|
for i, item := range s1 {
|
|
if item != s2[i] {
|
|
count++
|
|
}
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
func tryParse[T any](items []string, parse func(string) (T, error)) ([]T, error) {
|
|
res := make([]T, 0, len(items))
|
|
for i, item := range items {
|
|
parsed, err := parse(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid item #%d: %w", i+1, err)
|
|
}
|
|
|
|
res = append(res, parsed)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func mapKeysEqual[T comparable, U any](m1, m2 map[T]U) bool {
|
|
if len(m1) != len(m2) {
|
|
return false
|
|
}
|
|
|
|
for key := range m1 {
|
|
if _, ok := m2[key]; !ok {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|