176 lines
4.0 KiB
Go
176 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
type Coordinate struct {
|
|
row int
|
|
col int
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) != 2 && len(os.Args) != 3 {
|
|
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")
|
|
|
|
nodes, err := parseInputMatrix(inputLines)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to parse input: %s", err))
|
|
}
|
|
|
|
fmt.Printf("Part 1: %d\n", part1(nodes))
|
|
fmt.Printf("Part 2: %d\n", part2(nodes))
|
|
}
|
|
|
|
func part1(nodes []Coordinate) int {
|
|
expanded := expandUniverse(nodes, 2)
|
|
return computePairwiseDistanceTotal(expanded)
|
|
}
|
|
|
|
func part2(nodes []Coordinate) int {
|
|
expanded := expandUniverse(nodes, 1_000_000)
|
|
return computePairwiseDistanceTotal(expanded)
|
|
}
|
|
|
|
func computePairwiseDistanceTotal(nodes []Coordinate) int {
|
|
type Pair struct {
|
|
node1 Coordinate
|
|
node2 Coordinate
|
|
}
|
|
|
|
total := 0
|
|
for i := 0; i < len(nodes)-1; i++ {
|
|
for j := i + 1; j < len(nodes); j++ {
|
|
pair := Pair{node1: nodes[i], node2: nodes[j]}
|
|
distance := abs(pair.node2.col-pair.node1.col) + abs(pair.node2.row-pair.node1.row)
|
|
|
|
total += distance
|
|
}
|
|
}
|
|
|
|
return total
|
|
}
|
|
|
|
func expandUniverse(nodes []Coordinate, expansion int) []Coordinate {
|
|
rowExpanded := expandAlongAxis(
|
|
nodes,
|
|
expansion,
|
|
func(c Coordinate) int { return c.row },
|
|
func(c Coordinate, n int) Coordinate { return Coordinate{row: n, col: c.col} },
|
|
)
|
|
|
|
expanded := expandAlongAxis(
|
|
rowExpanded,
|
|
expansion,
|
|
func(c Coordinate) int { return c.col },
|
|
func(c Coordinate, n int) Coordinate { return Coordinate{row: c.row, col: n} },
|
|
)
|
|
|
|
return expanded
|
|
}
|
|
|
|
// expandAlongAxis will expand by the given amount the universe only along a single axis.
|
|
// getAxis will allow the function to get the value of a single axis, given a coordinate,
|
|
// and setAxis must return a new coordinate with the same axis set to the given value.
|
|
func expandAlongAxis(
|
|
nodes []Coordinate,
|
|
expansion int,
|
|
getAxis func(Coordinate) int,
|
|
setAxisValue func(Coordinate, int) Coordinate,
|
|
) []Coordinate {
|
|
expanded := slices.Clone(nodes)
|
|
slices.SortFunc(expanded, func(a, b Coordinate) int { return cmp.Compare(getAxis(a), getAxis(b)) })
|
|
|
|
blank := findBlankAxisValues(nodes, getAxis)
|
|
totalExpansion := 0
|
|
for _, blankIdx := range blank {
|
|
for i := range expanded {
|
|
axisValue := getAxis(expanded[i])
|
|
if axisValue <= blankIdx+totalExpansion {
|
|
continue
|
|
}
|
|
|
|
expanded[i] = setAxisValue(expanded[i], axisValue+(expansion-1))
|
|
}
|
|
totalExpansion += (expansion - 1)
|
|
}
|
|
|
|
return expanded
|
|
}
|
|
|
|
// findBlankAxisValues finds all the positions where all values along the given axis are blank
|
|
func findBlankAxisValues(nodes []Coordinate, getAxis func(Coordinate) int) []int {
|
|
maxCoord := slices.MaxFunc(nodes, func(node1, node2 Coordinate) int {
|
|
return cmp.Compare(getAxis(node1), getAxis(node2))
|
|
})
|
|
|
|
axisMax := getAxis(maxCoord)
|
|
knownAlongAxis := mapToSet(nodes, getAxis)
|
|
blank := []int{}
|
|
for i := 0; i <= axisMax; i++ {
|
|
if _, ok := knownAlongAxis[i]; !ok {
|
|
blank = append(blank, i)
|
|
}
|
|
}
|
|
|
|
return blank
|
|
}
|
|
|
|
// mapToSet maps all values of the given input and turns them into a set
|
|
func mapToSet[T any, U comparable, S ~[]T](items S, mapper func(T) U) map[U]struct{} {
|
|
res := map[U]struct{}{}
|
|
for _, item := range items {
|
|
key := mapper(item)
|
|
res[key] = struct{}{}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func abs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
|
|
return x
|
|
}
|
|
|
|
func parseInputMatrix(input []string) ([]Coordinate, error) {
|
|
nodes := []Coordinate{}
|
|
|
|
for row, line := range input {
|
|
for col, char := range line {
|
|
if char == '#' {
|
|
nodes = append(nodes, Coordinate{row: row, col: col})
|
|
} else if char != '.' {
|
|
return nil, fmt.Errorf("invalid input char %c", char)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nodes, nil
|
|
}
|