Skip to content

Commit

Permalink
working day16 part 2
Browse files Browse the repository at this point in the history
  • Loading branch information
jstensland committed Jan 3, 2025
1 parent 0a5c46f commit 6045139
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 61 deletions.
86 changes: 66 additions & 20 deletions 2024/day16/day16.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import (
"sort"
)

const (
turnCost = 1000
moveCost = 1
)

func SolvePart1(in io.Reader) (int, error) {
grid, err := ParseIn(in)
if err != nil {
Expand Down Expand Up @@ -38,47 +43,88 @@ func (g *Grid) BestRoute() int {
return minCost
}

// search will recursively search the next steps for each node
func (g *Grid) search(loc Position, cost int) int {
// fmt.Println("searching", loc, "cost:", cost)
func SolvePart2(in io.Reader) (int, error) {
grid, err := ParseIn(in)
if err != nil {
return 0, fmt.Errorf("error loading input: %w", err)
}

return grid.BestSeats(), nil
}

func (g *Grid) BestSeats() int {
// to start, we need to check all 4 directions, so check one ahead
backward := g.search(g.Start.Right().Right().Forward(), 0)
minCost := g.search(g.Start, 0)
if backward > 0 && backward < minCost {
minCost = backward
}

// now that we know the min cost, do the same thing, but record all the
// locations on the min cost route...
// This definitely relies on internal state of the grid, which isn't ideal, but is
// quite efficient the second time because the only the leastCost route continues
// due to accumlated state from the previous run
g.minAnswer = minCost
g.search(g.Start.Right().Right().Forward(), 0)
g.search(g.Start, 0)
return len(g.counted)
}

// search will recursively search the next steps for each node and return the cost
// of the current position plus getting to the next place
func (g *Grid) search(pos Position, cost int) int {
// if it's a wall, this is an impossible move
if g.GetLoc(loc) == Wall {
if g.GetLoc(pos) == Wall {
return -1
}
// if you've been here before... skip it
if lastCost, ok := g.visited[loc]; ok && lastCost < cost {
// if you've been here before for less, skip it
if lastCost, ok := g.visited[pos]; ok && lastCost < cost {
return -1 // got here for less some other way
}
g.visited[loc] = cost
g.visited[pos] = cost

// check if the cost is already beyond the known cheapest. Quit if so
if g.leastCost > 0 && cost > g.leastCost {
return -1
}
// check if you're at the ending location. Return accumulated cost if so.
if loc == g.End {
// fmt.Println("found end! cost:", cost)
if pos == g.End {
if cost < g.leastCost {
g.leastCost = cost
}

// for the second run...
if cost == g.minAnswer {
g.counted[pos.Location] = true // count the ending spot
}

return cost
}

// search cost from forward, right and left
fwd := g.search(loc.Forward(), cost+1)
right := g.search(loc.Right().Forward(), cost+1001)
left := g.search(loc.Left().Forward(), cost+1001)
// search cost from forward, right and left and select the lowest
costs := []int{
g.search(pos.Forward(), cost+moveCost),
g.search(pos.Right().Forward(), cost+moveCost+turnCost),
g.search(pos.Left().Forward(), cost+moveCost+turnCost),
}
nextCost := leastCost(costs)
if nextCost == g.minAnswer {
g.counted[pos.Location] = true
}
return nextCost
}

// select the lowest that's not -1
costs := []int{fwd, right, left}
sort.Ints(costs)
idx := slices.IndexFunc(costs, func(n int) bool {
// leastCost finds the samllest cost that's not -1. Returns -1 if no such cost
func leastCost(costs []int) int {
tmp := make([]int, len(costs)) // sort a copy for no side effects
copy(tmp, costs)
sort.Ints(tmp)
idx := slices.IndexFunc(tmp, func(n int) bool {
return n > 0
})
if idx == -1 {
return -1
}
// fmt.Println("searching", loc, "incoming cost:", cost)
// fmt.Println("cost is:", costs[idx])
return costs[idx]
return tmp[idx]
}
72 changes: 51 additions & 21 deletions 2024/day16/day16_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import (
)

func example() io.Reader {
return strings.NewReader(`###############
return strings.NewReader(
`###############
#.......#....E#
#.#.###.#.###.#
#.....#.#...#.#
Expand All @@ -30,14 +31,34 @@ func example() io.Reader {
###############`)
}

func example2() io.Reader {
return strings.NewReader(`#################
#...#...#...#..E#
#.#.#.#.#.#.#.#.#
#.#.#.#...#...#.#
#.#.#.#.###.#.#.#
#...#.#.#.....#.#
#.#.#.#.#.#####.#
#.#...#.#.#.....#
#.#.#####.#.###.#
#.#.#.......#...#
#.#.###.#####.###
#.#.#...#.....#.#
#.#.#.#####.###.#
#.#.#.........#.#
#.#.#.#########.#
#S#.............#
#################`)
}

func TestParseExample(t *testing.T) {
grid, err := day16.ParseIn(example())

require.NoError(t, err)
assert.Equal(t, 15, grid.Width)
assert.Equal(t, 15, grid.Height)
assert.Equal(t, day16.Position{13, 1, day16.East}, grid.Start)
assert.Equal(t, day16.Position{1, 13, day16.North}, grid.End)
assert.Equal(t, day16.Position{day16.Location{13, 1}, day16.East}, grid.Start)
assert.Equal(t, day16.Position{day16.Location{1, 13}, day16.North}, grid.End)
}

func TestExampleCost(t *testing.T) {
Expand All @@ -48,29 +69,13 @@ func TestExampleCost(t *testing.T) {
}

func TestExample2Cost(t *testing.T) {
grid, err := day16.ParseIn(strings.NewReader(`#################
#...#...#...#..E#
#.#.#.#.#.#.#.#.#
#.#.#.#...#...#.#
#.#.#.#.###.#.#.#
#...#.#.#.....#.#
#.#.#.#.#.#####.#
#.#...#.#.#.....#
#.#.#####.#.###.#
#.#.#.......#...#
#.#.###.#####.###
#.#.#...#.....#.#
#.#.#.#####.###.#
#.#.#.........#.#
#.#.#.#########.#
#S#.............#
#################`))
grid, err := day16.ParseIn(example2())

require.NoError(t, err)
assert.Equal(t, 11048, grid.BestRoute())
}

func TestPart2(t *testing.T) {
func TestPart1(t *testing.T) {
inFile := "./input.txt"
in, err := os.Open(inFile)
require.NoError(t, err)
Expand All @@ -80,3 +85,28 @@ func TestPart2(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 99488, result)
}

func TestBestSeatsExample(t *testing.T) {
grid, err := day16.ParseIn(example())

require.NoError(t, err)
assert.Equal(t, 45, grid.BestSeats())
}

func TestBestSeatsExample2(t *testing.T) {
grid, err := day16.ParseIn(example2())

require.NoError(t, err)
assert.Equal(t, 64, grid.BestSeats())
}

func TestPart2(t *testing.T) {
inFile := "./input.txt"
in, err := os.Open(inFile)
require.NoError(t, err)

result, err := day16.SolvePart2(in)

require.NoError(t, err)
assert.Equal(t, 516, result)
}
38 changes: 31 additions & 7 deletions 2024/day16/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ const (
)

type Position struct {
Row int
Col int
Location
Direction Orientation
}

type Location struct {
Row int
Col int
}

// Forward will add a vector to a location and returns the resulting location.
func (loc Position) Forward() Position {
var mv Move
Expand All @@ -50,17 +54,33 @@ func (loc Position) Forward() Position {
case West:
mv = Move{deltaEast: -1, deltaSouth: 0}
}
return Position{Row: loc.Row + mv.deltaSouth, Col: loc.Col + mv.deltaEast, Direction: loc.Direction}
return Position{
Location: Location{
Row: loc.Row + mv.deltaSouth,
Col: loc.Col + mv.deltaEast,
},
Direction: loc.Direction,
}
}

// Right returns the same position, turned to the right.
func (loc Position) Right() Position {
return Position{Row: loc.Row, Col: loc.Col, Direction: (loc.Direction + 1) % numDirections}
return Position{
Location: Location{
Row: loc.Row, Col: loc.Col,
},
Direction: (loc.Direction + 1) % numDirections,
}
}

// Left returns the same position, turned to the left..
func (loc Position) Left() Position {
return Position{Row: loc.Row, Col: loc.Col, Direction: ((loc.Direction - 1) + numDirections) % numDirections}
return Position{
Location: Location{
Row: loc.Row, Col: loc.Col,
},
Direction: ((loc.Direction - 1) + numDirections) % numDirections,
}
}

type Move struct {
Expand All @@ -78,6 +98,9 @@ type Grid struct {
leastCost int

visited map[Position]int

minAnswer int // should not be Grid state, just experimenting
counted map[Location]bool
}

func (g *Grid) GetLoc(l Position) State {
Expand Down Expand Up @@ -125,12 +148,13 @@ func ParseIn(in io.Reader) (*Grid, error) {

return &Grid{
data: data,
Start: Position{startRow, startCol, East},
End: Position{endRow, endCol, North}, // end orientation does not matter
Start: Position{Location{startRow, startCol}, East},
End: Position{Location{endRow, endCol}, North}, // end orientation does not matter
Width: len(data[0]),
Height: len(data),

visited: make(map[Position]int),
counted: make(map[Location]bool),
}, nil
}

Expand Down
39 changes: 27 additions & 12 deletions 2024/day16/input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,37 @@ import (
)

func TestRight(t *testing.T) {
assert.Equal(t, day16.Position{0, 0, day16.East}, day16.Position{0, 0, day16.North}.Right())
assert.Equal(t, day16.Position{0, 0, day16.South}, day16.Position{0, 0, day16.East}.Right())
assert.Equal(t, day16.Position{0, 0, day16.West}, day16.Position{0, 0, day16.South}.Right())
assert.Equal(t, day16.Position{0, 0, day16.North}, day16.Position{0, 0, day16.West}.Right())
zeroLoc := day16.Location{0, 0}
assert.Equal(t, day16.Position{zeroLoc, day16.East}, day16.Position{zeroLoc, day16.North}.Right())
assert.Equal(t, day16.Position{zeroLoc, day16.South}, day16.Position{zeroLoc, day16.East}.Right())
assert.Equal(t, day16.Position{zeroLoc, day16.West}, day16.Position{zeroLoc, day16.South}.Right())
assert.Equal(t, day16.Position{zeroLoc, day16.North}, day16.Position{zeroLoc, day16.West}.Right())
}

func TestLeft(t *testing.T) {
assert.Equal(t, day16.Position{0, 0, day16.West}, day16.Position{0, 0, day16.North}.Left())
assert.Equal(t, day16.Position{0, 0, day16.North}, day16.Position{0, 0, day16.East}.Left())
assert.Equal(t, day16.Position{0, 0, day16.East}, day16.Position{0, 0, day16.South}.Left())
assert.Equal(t, day16.Position{0, 0, day16.South}, day16.Position{0, 0, day16.West}.Left())
zeroLoc := day16.Location{0, 0}
assert.Equal(t, day16.Position{zeroLoc, day16.West}, day16.Position{zeroLoc, day16.North}.Left())
assert.Equal(t, day16.Position{zeroLoc, day16.North}, day16.Position{zeroLoc, day16.East}.Left())
assert.Equal(t, day16.Position{zeroLoc, day16.East}, day16.Position{zeroLoc, day16.South}.Left())
assert.Equal(t, day16.Position{zeroLoc, day16.South}, day16.Position{zeroLoc, day16.West}.Left())
}

func TestRightForward(t *testing.T) {
assert.Equal(t, day16.Position{3, 4, day16.East}, day16.Position{3, 3, day16.North}.Right().Forward())
assert.Equal(t, day16.Position{4, 3, day16.South}, day16.Position{3, 3, day16.East}.Right().Forward())
assert.Equal(t, day16.Position{3, 2, day16.West}, day16.Position{3, 3, day16.South}.Right().Forward())
assert.Equal(t, day16.Position{2, 3, day16.North}, day16.Position{3, 3, day16.West}.Right().Forward())
startLoc := day16.Location{3, 3}
assert.Equal(t,
day16.Position{day16.Location{3, 4}, day16.East},
day16.Position{startLoc, day16.North}.Right().Forward(),
)
assert.Equal(t,
day16.Position{day16.Location{4, 3}, day16.South},
day16.Position{startLoc, day16.East}.Right().Forward(),
)
assert.Equal(t,
day16.Position{day16.Location{3, 2}, day16.West},
day16.Position{startLoc, day16.South}.Right().Forward(),
)
assert.Equal(t,
day16.Position{day16.Location{2, 3}, day16.North},
day16.Position{startLoc, day16.West}.Right().Forward(),
)
}
2 changes: 1 addition & 1 deletion 2024/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func Run() error {
{"Day 15 Part 1", day15.SolvePart1, "./day15/input.txt"},
{"Day 15 Part 2", day15.SolvePart2, "./day15/input.txt"},
{"Day 16 Part 1", day16.SolvePart1, "./day16/input.txt"},
// {"Day 16 Part 2", day16.SolvePart2, "./day16/input.txt"},
{"Day 16 Part 2", day16.SolvePart2, "./day16/input.txt"},
} {
err := RunIt(day.name, day.fn, day.in)
if err != nil {
Expand Down

0 comments on commit 6045139

Please sign in to comment.