Skip to content

Commit

Permalink
2023 Day 21 alternative solution
Browse files Browse the repository at this point in the history
This solution is slower than the previous solution, but works for the example input cases as well as my input
  • Loading branch information
ictrobot committed Dec 21, 2023
1 parent c38da19 commit 6dcf95a
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 57 deletions.
159 changes: 121 additions & 38 deletions internal/aoc2023/day21/day21.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import (
"github.com/ictrobot/aoc/internal/util/numbers"
"github.com/ictrobot/aoc/internal/util/parse"
"github.com/ictrobot/aoc/internal/util/vec"
"math"
"golang.org/x/exp/maps"
)

//go:embed example
var Example string

type Day21 struct {
size int
grid [][]bool
start vec.I2[int]
example bool
Expand All @@ -20,12 +21,17 @@ type Day21 struct {
func (d *Day21) Parse(input string) {
lines := parse.Lines(input)

d.grid = make([][]bool, len(lines))
d.size = len(lines)
d.grid = make([][]bool, d.size)
d.start = vec.I2[int]{}
d.example = false

for y, l := range lines {
d.grid[y] = make([]bool, len(l))
if len(l) != len(lines) {
panic("grid must be square")
}

d.grid[y] = make([]bool, d.size)
for x, c := range l {
if c == 'S' {
d.start.X = x
Expand All @@ -48,82 +54,159 @@ func (d *Day21) Part1() any {
steps = 6
}

plots := d.plots(steps)
return plots[steps]
return d.reachablePlots(steps)
}

func (d *Day21) Part2() any {
steps := 26501365
if d.example {
return "part 2 unsupported for example input"
steps = 5000
}
// this only works because the start position is on a blank row & col

steps := 26501365
repeatAfter := len(d.grid) + len(d.grid[0])
return d.reachablePlots(steps)
}

func (d *Day21) reachablePlots(steps int) int64 {
repeatAfter := d.size
repeats := steps / repeatAfter
leftOver := steps % repeatAfter
extra := steps % repeatAfter

plots := d.plots(leftOver + repeatAfter + repeatAfter)
if repeats <= 6 {
firstReached := d.firstReached(steps)
xMin, yMin, xMax, yMax := firstReached.Bounds()
return reachable(firstReached, xMin, yMin, xMax, yMax, steps)
}

a, b, c := fitQuadratic([3]float64{0, 1, 2}, [3]float64{
float64(plots[leftOver]),
float64(plots[leftOver+repeatAfter]),
float64(plots[leftOver+repeatAfter+repeatAfter]),
})
// looking at the example input with steps=5000, repeats=454 and extra=6.
// after 4 repeats (6 + 4*11 = 50 steps) the number of reachable garden
// plots within each repeating grid looks like:
// -4, -4 4, -4 4x 2
// 0 0 0 6 17 9 0 0 0 4x 6
// 0 0 6 36 39 38 9 0 0 8x 9
// 0 6 36 39 42 39 38 9 0 2x 15
// 6 36 39 42 39 42 39 38 9 1x 17
// 18 39 42 39 42 39 42 39 15 1x 18
// 9 38 39 42 39 42 39 29 2 3x 29
// 0 9 38 39 42 39 29 2 0 3x 36
// 0 0 9 38 39 29 2 0 0 6x 38
// 0 0 0 9 15 2 0 0 0 16x 39
// -4, 4 4, 4 9x 42
//
// after 5 repeats (6 + 5*11 = 61 steps):
// -5, -5 5, -5 5x 2
// 0 0 0 0 6 17 9 0 0 0 0 5x 6
// 0 0 0 6 36 39 38 9 0 0 0 10x 9
// 0 0 6 36 39 42 39 38 9 0 0 2x 15
// 0 6 36 39 42 39 42 39 38 9 0 1x 17
// 6 36 39 42 39 42 39 42 39 38 9 1x 18
// 18 39 42 39 42 39 42 39 42 39 15 4x 29
// 9 38 39 42 39 42 39 42 39 29 2 4x 36
// 0 9 38 39 42 39 42 39 29 2 0 8x 38
// 0 0 9 38 39 42 39 29 2 0 0 25x 39
// 0 0 0 9 38 39 29 2 0 0 0 16x 42
// 0 0 0 0 9 15 2 0 0 0 0
// -5, 5 5, 5
//
// after 6 repeats (6 + 6*11 = 72 steps):
// -6, -6 6, -6 6x 2
// 0 0 0 0 0 6 17 9 0 0 0 0 0 6x 6
// 0 0 0 0 6 36 39 38 9 0 0 0 0 12x 9
// 0 0 0 6 36 39 42 39 38 9 0 0 0 2x 15
// 0 0 6 36 39 42 39 42 39 38 9 0 0 1x 17
// 0 6 36 39 42 39 42 39 42 39 38 9 0 1x 18
// 6 36 39 42 39 42 39 42 39 42 39 38 9 5x 29
// 18 39 42 39 42 39 42 39 42 39 42 39 15 5x 36
// 9 38 39 42 39 42 39 42 39 42 39 29 2 10x 38
// 0 9 38 39 42 39 42 39 42 39 29 2 0 36x 39
// 0 0 9 38 39 42 39 42 39 29 2 0 0 25x 42
// 0 0 0 9 38 39 42 39 29 2 0 0 0
// 0 0 0 0 9 38 39 29 2 0 0 0 0
// 0 0 0 0 0 9 15 2 0 0 0 0 0
// -6, 6 6, 6
//
// therefore extrapolate how many grids there are with each count from the
// pattern at the 4th, 5th and 6th repeat.

firstReached := d.firstReached(extra + 6*repeatAfter)

minX, minY, maxX, maxY := firstReached.Bounds()
minX = (minX - d.size + 1) / d.size * d.size
minY = (minY - d.size + 1) / d.size * d.size

repeatCounts := [...]map[int64]int64{make(map[int64]int64), make(map[int64]int64), make(map[int64]int64)}
for x := minX; x <= maxX; x += d.size {
for y := minY; y <= maxY; y += d.size {
for i := 0; i < 3; i++ {
c := reachable(firstReached, x, y, x+d.size-1, y+d.size-1, extra+(4+i)*repeatAfter)
if c > 0 {
repeatCounts[i][c]++
}
}
}
}

return evalQuadratic(a, b, c, repeats)
unique := maps.Keys(repeatCounts[0])
var total int64
for _, c := range unique {
total += c * extrapolate(repeatCounts[0][c], repeatCounts[1][c], repeatCounts[2][c], int64(repeats)-6)
}
return total
}

func (d *Day21) plots(steps int) []int64 {
func (d *Day21) firstReached(maxSteps int) *vec.Grid[int] {
// use Z to store the step the position is reached first
queue := []vec.I3[int]{d.start.WithZ(0)}
visited := vec.Grid[bool]{}
plots := make([]int64, steps+1)
reached := vec.NewGrid[int](d.start.X-maxSteps, d.start.Y-maxSteps, d.start.X+maxSteps, d.start.Y+maxSteps)

for len(queue) > 0 {
p := queue[0]
queue = queue[1:]

if !visited.SetIfZero(p.XY(), true) {
if !reached.SetIfZero(p.XY(), p.Z) {
continue
}

for step := p.Z; step <= steps; step += 2 {
plots[step]++
}

if p.Z >= steps {
if p.Z >= maxSteps {
continue
}

for _, dir := range vec.I2Directions {
n := p.Add(dir.WithZ(1))

if d.grid[numbers.IntMod(n.Y, len(d.grid))][numbers.IntMod(n.X, len(d.grid[0]))] {
if d.grid[numbers.IntMod(n.Y, d.size)][numbers.IntMod(n.X, d.size)] {
continue
}

if visited.Get(n.XY()) {
if reached.Contains(n.XY()) {
continue
}

queue = append(queue, n)
}
}

return plots
return reached
}

func fitQuadratic(x, y [3]float64) (a, b, c float64) {
a = (x[0]*(y[2]-y[1]) + x[1]*(y[0]-y[2]) + x[2]*(y[1]-y[0])) /
((x[0] - x[1]) * (x[0] - x[2]) * (x[1] - x[2]))
b = ((y[1] - y[0]) / (x[1] - x[0])) - a*(x[0]+x[1])
c = y[0] - (a * x[0] * x[0]) - (b * x[0])
func reachable(firstReached *vec.Grid[int], xMin, yMin, xMax, yMax, steps int) (plots int64) {
for x := xMin; x <= xMax; x++ {
for y := yMin; y <= yMax; y++ {
v := firstReached.GetInts(x, y)
if v > 0 && v <= steps && v%2 == steps%2 {
plots++
}
}
}
return
}

func evalQuadratic(a, b, c float64, steps int) int64 {
x := float64(steps)
f := a*x*x + b*x + c
return int64(math.Round(f))
func extrapolate(x0, x1, x2, n int64) int64 {
diff0 := x1 - x0
diff1 := x2 - x1

if diff0 == diff1 {
return x2 + n*diff0
}

return x2 + n*((n+3)*diff1-(n+1)*diff0)/2
}
67 changes: 49 additions & 18 deletions internal/aoc2023/day21/day21_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import (
)

const Part1 = 16

//const Part2 = 0
const Part2 = 16733044

func TestDay21_ParseExample(t *testing.T) {
d1 := Day21{}
Expand Down Expand Up @@ -44,19 +43,51 @@ func BenchmarkDay21_Part1(b *testing.B) {
}
}

//func TestDay21_Part2(t *testing.T) {
// d := Day21{}
// d.ParseExample()
//
// assert.EqualValues(t, Part2, d.Part2())
//}
//
//func BenchmarkDay21_Part2(b *testing.B) {
// d := Day21{}
// d.ParseExample()
//
// b.ResetTimer()
// for i := 0; i < b.N; i++ {
// assert.EqualValues(b, Part2, d.Part2())
// }
//}
func TestDay21_Part2(t *testing.T) {
d := Day21{}
d.ParseExample()

assert.EqualValues(t, Part2, d.Part2())
}

func BenchmarkDay21_Part2(b *testing.B) {
d := Day21{}
d.ParseExample()

b.ResetTimer()
for i := 0; i < b.N; i++ {
assert.EqualValues(b, Part2, d.Part2())
}
}

func TestDay21_reachablePlots(t *testing.T) {
d := Day21{}
d.ParseExample()
assert.EqualValues(t, 16, d.reachablePlots(6))
assert.EqualValues(t, 50, d.reachablePlots(10))
assert.EqualValues(t, 1594, d.reachablePlots(50))
assert.EqualValues(t, 6536, d.reachablePlots(100))
assert.EqualValues(t, 167004, d.reachablePlots(500))
assert.EqualValues(t, 668697, d.reachablePlots(1000))
assert.EqualValues(t, 16733044, d.reachablePlots(5000))
}

func Test_extrapolate(t *testing.T) {
assert.EqualValues(t, 1, extrapolate(1, 1, 1, 1))
assert.EqualValues(t, 1, extrapolate(1, 1, 1, 2))
assert.EqualValues(t, 1, extrapolate(1, 1, 1, 3))
assert.EqualValues(t, 1, extrapolate(1, 1, 1, 4))
assert.EqualValues(t, 1, extrapolate(1, 1, 1, 5))

assert.EqualValues(t, 9, extrapolate(3, 5, 7, 1))
assert.EqualValues(t, 11, extrapolate(3, 5, 7, 2))
assert.EqualValues(t, 13, extrapolate(3, 5, 7, 3))
assert.EqualValues(t, 15, extrapolate(3, 5, 7, 4))
assert.EqualValues(t, 17, extrapolate(3, 5, 7, 5))

assert.EqualValues(t, 49, extrapolate(16, 25, 36, 1))
assert.EqualValues(t, 64, extrapolate(16, 25, 36, 2))
assert.EqualValues(t, 81, extrapolate(16, 25, 36, 3))
assert.EqualValues(t, 100, extrapolate(16, 25, 36, 4))
assert.EqualValues(t, 121, extrapolate(16, 25, 36, 5))
}
2 changes: 1 addition & 1 deletion internal/util/vec/grid.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (g *Grid[T]) SetIfZero(v I2[int], t T) bool {
if !g.inBounds(v) {
if t == zero {
// avoid resizing grid when setting element outside bounds to zero
return false
return true
}
g.resizeToInclude(v.X, v.Y)
}
Expand Down

0 comments on commit 6dcf95a

Please sign in to comment.