Rewrite day 18 part 1 to use the shoelace formula

master
Nick Krichevsky 2023-12-19 13:20:42 -05:00
parent 75911aa9e3
commit 08a02dc99a
1 changed files with 86 additions and 96 deletions

View File

@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"io"
"math"
"os"
"regexp"
"slices"
"strconv"
"strings"
)
@ -31,6 +31,43 @@ type Coordinate struct {
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])
@ -66,125 +103,70 @@ func part1(plans []Plan) int {
}
drawn := drawPlans(plans)
emptySpaces := emptySpacesInDrawing(drawn)
for len(emptySpaces) > 0 {
empty := popMapKey(emptySpaces)
seen, outside := flood(drawn, empty)
if outside {
for _, pos := range seen {
delete(emptySpaces, pos)
}
} else {
return len(seen) + len(drawn)
}
verts, err := findVerts(drawn)
if err != nil {
panic(err)
}
panic("found no interior items")
area := 0
border := 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 += mat.Det()
border += abs(point2.Row-point1.Row) + abs(point2.Col-point1.Col)
}
// Why is this plus one necessary???
return (area+border)/2 + 1
}
func drawPlans(plans []Plan) map[Coordinate]string {
func drawPlans(plans []Plan) DrawnPlan {
cursor := Coordinate{Row: 0, Col: 0}
drawn := map[Coordinate]string{}
drawn := DrawnPlan{}
for _, plan := range plans {
for i := 0; i < plan.Count; i++ {
cursor = inDirection(cursor, plan.Direction)
drawn[cursor] = plan.ColorCode
}
r := NewRange(cursor, plan.Direction, plan.Count)
drawn = append(drawn, r)
cursor = r.End()
}
return drawn
}
func drawingBounds(drawn map[Coordinate]string) (minRow, maxRow, minCol, maxCol int) {
if len(drawn) == 0 {
panic("cannot get bounds of empty drawing")
}
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()
})
minRow = math.MaxInt
minCol = math.MaxInt
for position := range drawn {
minRow = min(position.Row, minRow)
minCol = min(position.Col, minCol)
maxRow = max(position.Row, maxRow)
maxCol = max(position.Col, maxCol)
}
return
}
func emptySpacesInDrawing(drawn map[Coordinate]string) map[Coordinate]struct{} {
minRow, maxRow, minCol, maxCol := drawingBounds(drawn)
emptySpaces := map[Coordinate]struct{}{}
for row := minRow; row <= maxRow; row++ {
for col := minCol; col <= maxCol; col++ {
position := Coordinate{Row: row, Col: col}
_, ok := drawn[position]
if !ok {
emptySpaces[position] = struct{}{}
}
}
}
return emptySpaces
}
// flood is a floodFill algorithm that will return the empty tiles flooded, and whether or not the exterior
// border was hit.
func flood(drawn map[Coordinate]string, seed Coordinate) (flooded []Coordinate, outside bool) {
if _, ok := drawn[seed]; ok {
// We can't flood a fille space
return []Coordinate{}, true
}
outsideHit := false
minRow, maxRow, minCol, maxCol := drawingBounds(drawn)
visited := map[Coordinate]struct{}{}
emptyVisited := []Coordinate{}
toVisit := []Coordinate{seed}
for len(toVisit) > 0 {
visiting := toVisit[0]
toVisit = toVisit[1:]
if _, ok := visited[visiting]; ok {
continue
if idx == -1 {
return nil, errors.New("input is not a loop")
}
visited[visiting] = struct{}{}
emptyVisited = append(emptyVisited, visiting)
cursor = drawn[idx]
verts = append(verts, cursor.Start())
for _, neighbor := range neighbors(visiting) {
if _, ok := drawn[neighbor]; ok {
continue
} else if neighbor.Row < minRow || neighbor.Row > maxRow || neighbor.Col < minCol || neighbor.Col > maxCol {
outsideHit = true
continue
}
toVisit = append(toVisit, neighbor)
if cursor.Start() == startingPoint {
return verts, nil
}
}
return emptyVisited, outsideHit
}
func neighbors(coordinate Coordinate) []Coordinate {
return []Coordinate{
{Row: coordinate.Row + 1, Col: coordinate.Col},
{Row: coordinate.Row - 1, Col: coordinate.Col},
{Row: coordinate.Row, Col: coordinate.Col + 1},
{Row: coordinate.Row, Col: coordinate.Col - 1},
}
}
func inDirection(coordinate Coordinate, direction Direction) Coordinate {
func inDirection(coordinate Coordinate, direction Direction, n int) Coordinate {
switch direction {
case DirectionUp:
return Coordinate{Row: coordinate.Row - 1, Col: coordinate.Col}
return Coordinate{Row: coordinate.Row - n, Col: coordinate.Col}
case DirectionDown:
return Coordinate{Row: coordinate.Row + 1, Col: coordinate.Col}
return Coordinate{Row: coordinate.Row + n, Col: coordinate.Col}
case DirectionLeft:
return Coordinate{Row: coordinate.Row, Col: coordinate.Col - 1}
return Coordinate{Row: coordinate.Row, Col: coordinate.Col - n}
case DirectionRight:
return Coordinate{Row: coordinate.Row, Col: coordinate.Col + 1}
return Coordinate{Row: coordinate.Row, Col: coordinate.Col + n}
default:
panic(fmt.Sprintf("invalid direction %d", direction))
}
@ -265,3 +247,11 @@ func popMapKey[T comparable, U any](m map[T]U) T {
panic("cannot pop empty map")
}
func abs(n int) int {
if n < 0 {
return -n
}
return n
}