diff --git a/2024/day16/day16.go b/2024/day16/day16.go index cf6ce7b..20c5083 100644 --- a/2024/day16/day16.go +++ b/2024/day16/day16.go @@ -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 { @@ -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] } diff --git a/2024/day16/day16_test.go b/2024/day16/day16_test.go index e37f296..5bcff46 100644 --- a/2024/day16/day16_test.go +++ b/2024/day16/day16_test.go @@ -13,7 +13,8 @@ import ( ) func example() io.Reader { - return strings.NewReader(`############### + return strings.NewReader( + `############### #.......#....E# #.#.###.#.###.# #.....#.#...#.# @@ -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) { @@ -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) @@ -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) +} diff --git a/2024/day16/input.go b/2024/day16/input.go index 0bb8b62..ebad939 100644 --- a/2024/day16/input.go +++ b/2024/day16/input.go @@ -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 @@ -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 { @@ -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 { @@ -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 } diff --git a/2024/day16/input_test.go b/2024/day16/input_test.go index 8730af2..e205b87 100644 --- a/2024/day16/input_test.go +++ b/2024/day16/input_test.go @@ -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(), + ) } diff --git a/2024/runner/runner.go b/2024/runner/runner.go index fb0d1a0..4770e05 100644 --- a/2024/runner/runner.go +++ b/2024/runner/runner.go @@ -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 {