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

305 lines
6.4 KiB
Go

package main
import (
"errors"
"fmt"
"io"
"os"
"regexp"
"slices"
"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
}
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))
fmt.Printf("Part 2: %d\n", part2(plans))
}
func part1(plans []Plan) int64 {
drawn := drawPlans(plans)
verts, err := findVerts(drawn)
if err != nil {
panic(err)
}
return shoelaceArea(verts)
}
func part2(plans []Plan) int64 {
updPlans, err := convertPlansForPart2(plans)
if err != nil {
panic(err)
}
drawn := drawPlans(updPlans)
verts, err := findVerts(drawn)
if err != nil {
panic(err)
}
return shoelaceArea(verts)
}
func shoelaceArea(verts []Coordinate) int64 {
area := int64(0)
border := int64(0)
for i := 0; i < len(verts); i++ {
point1 := verts[i]
point2 := verts[(i+1)%len(verts)]
mat := NewMatrix(point1.Col, point2.Col, point1.Row, point2.Row)
area += int64(mat.Det())
border += int64(abs(point2.Row-point1.Row) + abs(point2.Col-point1.Col))
}
// I arrived at this with a bit of guessing, and I don't think my reasoning is 100% solid (but it turns out
// this is actually Pick's thereom, which is cool)
//
// - area/2 comes from shoelace thereom
// - divide the borer by two because our border tiles have "width" that we don't want to double count
// - add 1 to include the starting point (this I am the least confident about but I was off-by-one)
return (area/2 + border/2) + 1
}
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()
}
return drawn
}
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")
}
cursor = drawn[idx]
verts = append(verts, cursor.Start())
if cursor.Start() == startingPoint {
return verts, nil
}
}
}
func inDirection(coordinate Coordinate, direction Direction, n int) Coordinate {
switch direction {
case DirectionUp:
return Coordinate{Row: coordinate.Row - n, Col: coordinate.Col}
case DirectionDown:
return Coordinate{Row: coordinate.Row + n, Col: coordinate.Col}
case DirectionLeft:
return Coordinate{Row: coordinate.Row, Col: coordinate.Col - n}
case DirectionRight:
return Coordinate{Row: coordinate.Row, Col: coordinate.Col + n}
default:
panic(fmt.Sprintf("invalid direction %d", direction))
}
}
func convertPlansForPart2(plans []Plan) ([]Plan, error) {
res := make([]Plan, len(plans))
for i, plan := range plans {
if len(plan.ColorCode) < 2 {
return nil, fmt.Errorf("item %d contained too view items in its color code", i)
}
directionCode := plan.ColorCode[len(plan.ColorCode)-1]
direction, err := directionFromNumericValue(string(directionCode))
if err != nil {
return nil, fmt.Errorf("item %d contained an invalid direction directive %c", i, directionCode)
}
encodedCount := plan.ColorCode[:len(plan.ColorCode)-1]
count, err := strconv.ParseInt(encodedCount, 16, 0)
if err != nil {
return nil, fmt.Errorf("item %d contained an invalid count directive %s", i, encodedCount)
}
res[i] = Plan{
Direction: direction,
Count: int(count),
ColorCode: plan.ColorCode,
}
}
return res, nil
}
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: %s", 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 directionFromNumericValue(n string) (Direction, error) {
switch n {
case "0":
return DirectionRight, nil
case "1":
return DirectionDown, nil
case "2":
return DirectionLeft, nil
case "3":
return DirectionUp, nil
default:
return DirectionUp, errors.New("invalid direction number")
}
}
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 abs(n int) int {
if n < 0 {
return -n
}
return n
}