Advent-of-Code/year25/day8.go
2025-12-08 19:01:06 +02:00

185 lines
4.0 KiB
Go

package year25
import (
"cmp"
"fmt"
"log"
"math"
"os"
"slices"
"strconv"
"git.bizdoc.ro/gabi-public/Advent-of-Code.git/aoc"
"git.bizdoc.ro/private/devkit.git/collections/geometry/v2"
)
type ClusterId int
type ClustersDB[T any] struct { // TODO: move to devkit
elementIdToCluster []ClusterId
clusterToSize []int
}
func (s *ClustersDB[T]) GetClusterId(elementId int) ClusterId {
return s.elementIdToCluster[elementId]
}
func (s *ClustersDB[T]) Move(elementId int, to ClusterId) {
elementCluster := s.GetClusterId(elementId)
if elementCluster != to {
s.clusterToSize[elementCluster] -= 1
s.clusterToSize[to] += 1
if s.clusterToSize[elementCluster] < 0 {
panic("invalid cluster to size")
}
s.elementIdToCluster[elementId] = to
}
}
func (s *ClustersDB[T]) JoinClustersByElements(a, b int) {
elementCluster1 := s.GetClusterId(a)
elementCluster2 := s.GetClusterId(b)
for el, clusterId := range s.elementIdToCluster {
if clusterId == elementCluster2 {
s.Move(el, elementCluster1)
}
}
}
func NewClustersDB[T any](data []T) *ClustersDB[T] {
points := make([]ClusterId, len(data))
sizes := make([]int, len(data))
for i := range points {
points[i] = ClusterId(i)
sizes[i] = 1
}
return &ClustersDB[T]{
elementIdToCluster: points,
clusterToSize: sizes,
}
}
func Day8Part1(ctx aoc.Context, steps int) (int, error) {
ctx.Logger = log.New(os.Stdout, "", 0)
boxes, err := day8ParseInput(ctx)
if err != nil {
return 0, fmt.Errorf("day8: failed to parse input %w", err)
}
sm := NewClustersDB(boxes)
var distances = SortedDistances(boxes)
for _, d := range distances[:steps] {
el1Id, el2Id, distance := d.E1, d.E2, d.Distance
if distance != 0 && (sm.GetClusterId(el1Id) != sm.GetClusterId(el2Id)) {
sm.JoinClustersByElements(el1Id, el2Id)
}
}
ints := slices.Clone(sm.clusterToSize)
slices.Sort(ints)
sol := 1
for _, i := range ints[len(ints)-3:] {
sol *= i
}
return sol, nil
}
func Day8Part2(ctx aoc.Context) (int, error) {
ctx.Logger = log.New(os.Stdout, "", 0)
boxes, err := day8ParseInput(ctx)
if err != nil {
return 0, fmt.Errorf("day8: failed to parse input %w", err)
}
sm := NewClustersDB(boxes)
var distances = SortedDistances(boxes)
for _, d := range distances {
el1Id, el2Id, distance := d.E1, d.E2, d.Distance
if distance != 0 && (sm.GetClusterId(el1Id) != sm.GetClusterId(el2Id)) {
sm.JoinClustersByElements(el1Id, el2Id)
if sm.clusterToSize[sm.GetClusterId(el1Id)] == len(boxes) {
return int(boxes[el1Id][0]) * int(boxes[el2Id][0]), nil
}
}
}
panic("unreachable")
}
func day8ParseInput(c aoc.Context) ([]Vector, error) {
g, err := geometry.ReaderToIntsGrid(c.Body, ",")
if err != nil {
return nil, fmt.Errorf("day8: failed to read input %w", err)
}
points := make([]Vector, g.Rows())
for i := range points {
points[i] = make(Vector, g.Cols())
for j := range points[i] {
v := g.At(geometry.NewPoint(i, j))
points[i][j] = float64(v)
}
}
return points, nil
}
type Vector []float64 // TODO: move to devkit
func (v Vector) String() string {
x := make([]string, len(v))
for i, v := range v {
x[i] = strconv.FormatFloat(v, 'f', -1, 64)
}
return fmt.Sprintf("[%g,%g,%g]", v[0], v[1], v[2])
}
func SquaredDelta(b, a Vector, dim int) float64 {
x := b[dim] - a[dim]
return x * x
}
func SquaredEuclideanDistance(a, b Vector) (s float64) {
for i := range a {
s += SquaredDelta(a, b, i)
}
return
}
func EuclideanDistance(a, b Vector) float64 {
return math.Sqrt(SquaredEuclideanDistance(a, b))
}
type Pair struct {
E1, E2 int
Distance float64
}
func SortedDistances(points []Vector) []Pair {
n := len(points)
pairs := make([]Pair, 0, n*(n-1)/2)
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
pairs = append(pairs, Pair{
E1: i,
E2: j,
// no need to spend time on sqrt. order does not change
Distance: SquaredEuclideanDistance(points[i], points[j]),
})
}
}
slices.SortFunc(pairs, func(a, b Pair) int {
return cmp.Compare(a.Distance, b.Distance)
})
return pairs
}