advent-of-code-2023/day18/main.go

258 lines
4.9 KiB
Go
Raw Normal View History

2023-12-19 02:35:49 +00:00
package main
import (
"errors"
"fmt"
"io"
"os"
"regexp"
"slices"
2023-12-19 02:35:49 +00:00
"strconv"
"strings"
)
type Direction int
const (
DirectionUp = iota
DirectionDown
DirectionLeft
DirectionRight
)
type Plan struct {
Direction Direction
Count int
ColorCode string
}
type Coordinate struct {
Row int
Col int
}
type Range struct {
// start and end are inclusive
start Coordinate
end Coordinate
}
type DrawnPlan []Range
type Matrix [2][2]int
func NewMatrix(a, b, c, d int) Matrix {
return [2][2]int{
{a, b},
{c, d},
}
}
func NewRange(start Coordinate, direction Direction, count int) Range {
end := inDirection(start, direction, count)
return Range{
start: start,
end: end,
}
}
func (mat Matrix) Det() int {
return mat[0][0]*mat[1][1] - mat[0][1]*mat[1][0]
}
func (r Range) Start() Coordinate {
return r.start
}
func (r Range) End() Coordinate {
return r.end
}
2023-12-19 02:35:49 +00:00
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")
plans, err := parsePlans(inputLines)
if err != nil {
panic(fmt.Sprintf("failed to parse input: %s", err))
}
fmt.Printf("Part 1: %d\n", part1(plans))
}
func part1(plans []Plan) int {
if len(plans) == 0 {
panic("cannot draw no plans!")
}
drawn := drawPlans(plans)
verts, err := findVerts(drawn)
if err != nil {
panic(err)
2023-12-19 02:35:49 +00:00
}
area := 0
border := 0
for i := 0; i < len(verts); i++ {
point1 := verts[i]
point2 := verts[(i+1)%len(verts)]
2023-12-19 02:35:49 +00:00
mat := NewMatrix(point1.Col, point2.Col, point1.Row, point2.Row)
area += mat.Det()
border += abs(point2.Row-point1.Row) + abs(point2.Col-point1.Col)
2023-12-19 02:35:49 +00:00
}
// Why is this plus one necessary???
return (area+border)/2 + 1
2023-12-19 02:35:49 +00:00
}
func drawPlans(plans []Plan) DrawnPlan {
cursor := Coordinate{Row: 0, Col: 0}
drawn := DrawnPlan{}
for _, plan := range plans {
r := NewRange(cursor, plan.Direction, plan.Count)
drawn = append(drawn, r)
cursor = r.End()
2023-12-19 02:35:49 +00:00
}
return drawn
2023-12-19 02:35:49 +00:00
}
func findVerts(drawn DrawnPlan) ([]Coordinate, error) {
startingPoint := drawn[0].Start()
cursor := drawn[0]
verts := []Coordinate{}
for {
idx := slices.IndexFunc(drawn, func(r Range) bool {
return r.Start() == cursor.End()
})
if idx == -1 {
return nil, errors.New("input is not a loop")
2023-12-19 02:35:49 +00:00
}
cursor = drawn[idx]
verts = append(verts, cursor.Start())
2023-12-19 02:35:49 +00:00
if cursor.Start() == startingPoint {
return verts, nil
2023-12-19 02:35:49 +00:00
}
}
}
func inDirection(coordinate Coordinate, direction Direction, n int) Coordinate {
2023-12-19 02:35:49 +00:00
switch direction {
case DirectionUp:
return Coordinate{Row: coordinate.Row - n, Col: coordinate.Col}
2023-12-19 02:35:49 +00:00
case DirectionDown:
return Coordinate{Row: coordinate.Row + n, Col: coordinate.Col}
2023-12-19 02:35:49 +00:00
case DirectionLeft:
return Coordinate{Row: coordinate.Row, Col: coordinate.Col - n}
2023-12-19 02:35:49 +00:00
case DirectionRight:
return Coordinate{Row: coordinate.Row, Col: coordinate.Col + n}
2023-12-19 02:35:49 +00:00
default:
panic(fmt.Sprintf("invalid direction %d", direction))
}
}
func parsePlans(inputLines []string) ([]Plan, error) {
return tryParse(inputLines, parsePlan)
}
func parsePlan(rawPlan string) (Plan, error) {
pattern := regexp.MustCompile(`^([RUDL]) (\d+) \(#([a-z0-f]+)\)`)
matches := pattern.FindStringSubmatch(rawPlan)
if matches == nil {
return Plan{}, fmt.Errorf("malformed pattern")
}
direction, err := directionFromAcronym(matches[1])
if err != nil {
return Plan{}, fmt.Errorf("invalid direction %s: %w", matches[1], err)
}
count, err := strconv.Atoi(matches[2])
if err != nil {
// Can't happen by the expression pattern
panic(fmt.Sprintf("failed to parse %s as number: %w", matches[2], err))
}
return Plan{
Direction: direction,
Count: count,
ColorCode: matches[3],
}, nil
}
func directionFromAcronym(n string) (Direction, error) {
switch n {
case "R":
return DirectionRight, nil
case "U":
return DirectionUp, nil
case "D":
return DirectionDown, nil
case "L":
return DirectionLeft, nil
default:
return DirectionUp, errors.New("invalid direction acronym")
}
}
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 mapKeys[T comparable, U any](m map[T]U) []T {
res := make([]T, 0, len(m))
for key := range m {
res = append(res, key)
}
return res
}
func popMapKey[T comparable, U any](m map[T]U) T {
for key := range m {
delete(m, key)
return key
}
panic("cannot pop empty map")
}
func abs(n int) int {
if n < 0 {
return -n
}
return n
}