diff --git a/day-21/benchmark b/day-21/benchmark new file mode 100644 index 0000000..a7ec0c4 --- /dev/null +++ b/day-21/benchmark @@ -0,0 +1,15 @@ +goos: linux +goarch: amd64 +pkg: github.com/onyx-and-iris/aoc2024/day-21 +cpu: Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz +BenchmarkSolve-12 1000000000 0.0008915 ns/op +BenchmarkSolve-12 1000000000 0.0007801 ns/op +BenchmarkSolve-12 1000000000 0.0006902 ns/op +BenchmarkSolve-12 1000000000 0.0009074 ns/op +BenchmarkSolve-12 1000000000 0.001131 ns/op +BenchmarkSolve-12 1000000000 0.0008011 ns/op +BenchmarkSolve-12 1000000000 0.001154 ns/op +BenchmarkSolve-12 1000000000 0.0009199 ns/op +BenchmarkSolve-12 1000000000 0.0008066 ns/op +BenchmarkSolve-12 1000000000 0.0008437 ns/op +ok github.com/onyx-and-iris/aoc2024/day-21 0.099s diff --git a/day-21/cmd/cli/main.go b/day-21/cmd/cli/main.go new file mode 100644 index 0000000..c85a91a --- /dev/null +++ b/day-21/cmd/cli/main.go @@ -0,0 +1,41 @@ +/******************************************************************************** + Advent of Code 2024 - day-21 +********************************************************************************/ + +package main + +import ( + "embed" + "flag" + "fmt" + "slices" + + log "github.com/sirupsen/logrus" + + problems "github.com/onyx-and-iris/aoc2024/day-21" +) + +//go:embed testdata +var files embed.FS + +func main() { + filename := flag.String("f", "input.txt", "input file") + loglevel := flag.Int("l", int(log.InfoLevel), "log level") + flag.Parse() + + if slices.Contains(log.AllLevels, log.Level(*loglevel)) { + log.SetLevel(log.Level(*loglevel)) + } + + data, err := files.ReadFile(fmt.Sprintf("testdata/%s", *filename)) + if err != nil { + log.Fatal(err) + } + + one, two, err := problems.Solve(data) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("solution one: %d\nsolution two: %d\n", one, two) +} diff --git a/day-21/go.mod b/day-21/go.mod new file mode 100644 index 0000000..3387108 --- /dev/null +++ b/day-21/go.mod @@ -0,0 +1,7 @@ +module github.com/onyx-and-iris/aoc2024/day-21 + +go 1.23.3 + +require github.com/sirupsen/logrus v1.9.3 + +require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect diff --git a/day-21/go.sum b/day-21/go.sum new file mode 100644 index 0000000..21f9bfb --- /dev/null +++ b/day-21/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/day-21/internal/one/benchmark b/day-21/internal/one/benchmark new file mode 100644 index 0000000..08533ec --- /dev/null +++ b/day-21/internal/one/benchmark @@ -0,0 +1,6 @@ +goos: linux +goarch: amd64 +pkg: github.com/onyx-and-iris/aoc2024/day-21/internal/one +cpu: Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz +BenchmarkSolve-12 1000000000 0.0000606 ns/op +ok github.com/onyx-and-iris/aoc2024/day-21/internal/one 0.008s diff --git a/day-21/internal/one/solve.go b/day-21/internal/one/solve.go new file mode 100644 index 0000000..fff991a --- /dev/null +++ b/day-21/internal/one/solve.go @@ -0,0 +1,36 @@ +package one + +import ( + "bytes" + + "github.com/onyx-and-iris/aoc2024/day-21/internal/pad" + log "github.com/sirupsen/logrus" +) + +func Solve(buf []byte) (int, error) { + r := bytes.NewReader(buf) + codes, err := parseLines(r) + if err != nil { + return 0, err + } + + numpad := pad.NewNumpad() + dirpad := pad.NewDirpad() + + var complexity int + for _, code := range codes { + moves := generateMoves(numpad, dirpad, code) + complexity += len(moves) * numFromCode(code) + log.Debugf("%s\n%d * %d\n", moves, len(moves), numFromCode(code)) + } + + return complexity, nil +} + +type generator interface { + Generate(code string) string +} + +func generateMoves(n, d generator, code string) string { + return d.Generate(d.Generate(n.Generate(code))) +} diff --git a/day-21/internal/one/solve_internal_test.go b/day-21/internal/one/solve_internal_test.go new file mode 100644 index 0000000..e5fd7ea --- /dev/null +++ b/day-21/internal/one/solve_internal_test.go @@ -0,0 +1,15 @@ +package one + +import ( + _ "embed" + "os" + "testing" +) + +//go:embed testdata/input.txt +var data []byte + +func BenchmarkSolve(b *testing.B) { + os.Stdout, _ = os.Open(os.DevNull) + Solve(data) +} diff --git a/day-21/internal/one/util.go b/day-21/internal/one/util.go new file mode 100644 index 0000000..061c3d1 --- /dev/null +++ b/day-21/internal/one/util.go @@ -0,0 +1,27 @@ +package one + +import ( + "bufio" + "io" + "strconv" +) + +func parseLines(r io.Reader) ([]string, error) { + codes := []string{} + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + codes = append(codes, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return codes, nil +} + +func numFromCode(s string) int { + n, _ := strconv.Atoi(s[:len(s)-1]) + return n +} diff --git a/day-21/internal/pad/dirpad.go b/day-21/internal/pad/dirpad.go new file mode 100644 index 0000000..d2aa1ab --- /dev/null +++ b/day-21/internal/pad/dirpad.go @@ -0,0 +1,106 @@ +package pad + +import ( + "slices" + "strings" + + log "github.com/sirupsen/logrus" +) + +type Dirpad struct { + current node + pad [][]rune + locations map[rune]coords +} + +func NewDirpad() *Dirpad { + pad := [][]rune{ + {'.', '^', 'A'}, + {'<', 'v', '>'}, + } + locations := make(map[rune]coords) + for y, row := range pad { + for x, value := range row { + locations[value] = coords{x, y} + } + } + + return &Dirpad{ + current: newNode(2, 0, 'A'), + pad: pad, + locations: locations, + } +} + +func (dp *Dirpad) valueAt(c coords) rune { + return dp.pad[c.y][c.x] +} + +func (dp *Dirpad) nextTarget(c rune) node { + coords := dp.locations[c] + return newNode(coords.x, coords.y, c) +} + +func (dp *Dirpad) Generate(code string) string { + var sb strings.Builder + for _, c := range code { + target := dp.nextTarget(c) + moved := newMove(dp.current.x, target.x, dp.current.y, target.y) + + var hb, vb strings.Builder + + for range absInt(moved.x) { + if moved.x >= 0 { + hb.WriteRune('>') + } else { + hb.WriteRune('<') + } + } + + for range absInt(moved.y) { + if moved.y <= 0 { + vb.WriteRune('^') + } else { + vb.WriteRune('v') + } + } + + if dp.valueAt(coords{dp.current.x + moved.x, dp.current.y}) == '.' { + sb.WriteString(vb.String()) + sb.WriteString(hb.String()) + } else if dp.valueAt(coords{dp.current.x, dp.current.y + moved.y}) == '.' { + sb.WriteString(hb.String()) + sb.WriteString(vb.String()) + } else if moved.x < 0 { + sb.WriteString(hb.String()) + sb.WriteString(vb.String()) + } else if moved.x >= 0 { + sb.WriteString(vb.String()) + sb.WriteString(hb.String()) + } + + sb.WriteRune('A') + dp.current = target + } + + log.Tracef("\n%s\n", dp.trace(sb.String())) + + return sb.String() +} + +func (dp *Dirpad) trace(visited string) string { + log.Debug(visited) + temp := slices.Clone(dp.pad) + + next := coords{2, 0} + for _, c := range visited { + next = neighbours(next, c) + temp[next.y][next.x] = 'X' + } + + output := []string{} + for _, row := range temp { + output = append(output, string(row)) + } + return strings.Join(output, "\n") +} diff --git a/day-21/internal/pad/move.go b/day-21/internal/pad/move.go new file mode 100644 index 0000000..0ea3e66 --- /dev/null +++ b/day-21/internal/pad/move.go @@ -0,0 +1,12 @@ +package pad + +type move struct { + x, y int +} + +func newMove(x1, x2, y1, y2 int) *move { + return &move{ + x: x2 - x1, + y: y2 - y1, + } +} diff --git a/day-21/internal/pad/neighbours.go b/day-21/internal/pad/neighbours.go new file mode 100644 index 0000000..ac31895 --- /dev/null +++ b/day-21/internal/pad/neighbours.go @@ -0,0 +1,16 @@ +package pad + +func neighbours(c coords, r rune) coords { + switch r { + case '^': + return coords{c.x, c.y - 1} + case '>': + return coords{c.x + 1, c.y} + case 'v': + return coords{c.x, c.y + 1} + case '<': + return coords{c.x - 1, c.y} + default: + return c + } +} diff --git a/day-21/internal/pad/node.go b/day-21/internal/pad/node.go new file mode 100644 index 0000000..5545733 --- /dev/null +++ b/day-21/internal/pad/node.go @@ -0,0 +1,14 @@ +package pad + +type coords struct { + x, y int +} + +type node struct { + coords + value rune +} + +func newNode(x, y int, value rune) node { + return node{coords: coords{x, y}, value: value} +} diff --git a/day-21/internal/pad/numpad.go b/day-21/internal/pad/numpad.go new file mode 100644 index 0000000..b9d2efe --- /dev/null +++ b/day-21/internal/pad/numpad.go @@ -0,0 +1,108 @@ +package pad + +import ( + "slices" + "strings" + + log "github.com/sirupsen/logrus" +) + +type Numpad struct { + current node + pad [][]rune + locations map[rune]coords +} + +func NewNumpad() *Numpad { + pad := [][]rune{ + {'7', '8', '9'}, + {'4', '5', '6'}, + {'1', '2', '3'}, + {'.', '0', 'A'}, + } + locations := make(map[rune]coords) + for y, row := range pad { + for x, value := range row { + locations[value] = coords{x, y} + } + } + + return &Numpad{ + current: newNode(2, 3, 'A'), + pad: pad, + locations: locations, + } +} + +func (np *Numpad) valueAt(c coords) rune { + return np.pad[c.y][c.x] +} + +func (np *Numpad) nextTarget(c rune) node { + coords := np.locations[c] + return newNode(coords.x, coords.y, c) +} + +func (np *Numpad) Generate(code string) string { + var sb strings.Builder + for _, c := range code { + target := np.nextTarget(c) + moved := newMove(np.current.x, target.x, np.current.y, target.y) + + var hb, vb strings.Builder + + for range absInt(moved.x) { + if moved.x >= 0 { + hb.WriteRune('>') + } else { + hb.WriteRune('<') + } + } + + for range absInt(moved.y) { + if moved.y <= 0 { + vb.WriteRune('^') + } else { + vb.WriteRune('v') + } + } + + if np.valueAt(coords{np.current.x + moved.x, np.current.y}) == '.' { + sb.WriteString(vb.String()) + sb.WriteString(hb.String()) + } else if np.valueAt(coords{np.current.x, np.current.y + moved.y}) == '.' { + sb.WriteString(hb.String()) + sb.WriteString(vb.String()) + } else if moved.x < 0 { + sb.WriteString(hb.String()) + sb.WriteString(vb.String()) + } else if moved.x >= 0 { + sb.WriteString(vb.String()) + sb.WriteString(hb.String()) + } + + sb.WriteRune('A') + np.current = target + } + + log.Tracef("\n%s\n", np.trace(sb.String())) + + return sb.String() +} + +func (np *Numpad) trace(visited string) string { + log.Debug(visited) + temp := slices.Clone(np.pad) + + next := coords{2, 3} + for _, c := range visited { + next = neighbours(next, c) + temp[next.y][next.x] = 'X' + } + + output := []string{} + for _, row := range temp { + output = append(output, string(row)) + } + return strings.Join(output, "\n") +} diff --git a/day-21/internal/pad/util.go b/day-21/internal/pad/util.go new file mode 100644 index 0000000..97b86bd --- /dev/null +++ b/day-21/internal/pad/util.go @@ -0,0 +1,7 @@ +package pad + +import "math" + +func absInt(n int) int { + return int(math.Abs(float64(n))) +} diff --git a/day-21/internal/two/benchmark b/day-21/internal/two/benchmark new file mode 100644 index 0000000..af72586 --- /dev/null +++ b/day-21/internal/two/benchmark @@ -0,0 +1,6 @@ +goos: linux +goarch: amd64 +pkg: github.com/onyx-and-iris/aoc2024/day-21/internal/two +cpu: Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz +BenchmarkSolve-12 1000000000 0.0007436 ns/op +ok github.com/onyx-and-iris/aoc2024/day-21/internal/two 0.011s diff --git a/day-21/internal/two/cache.go b/day-21/internal/two/cache.go new file mode 100644 index 0000000..a9bac56 --- /dev/null +++ b/day-21/internal/two/cache.go @@ -0,0 +1,41 @@ +package two + +import "sync" + +type robotCache struct { + mu *sync.RWMutex + data map[string]map[int]int +} + +func newRobotCache() robotCache { + return robotCache{ + mu: &sync.RWMutex{}, + data: make(map[string]map[int]int), + } +} + +func (rc *robotCache) read(moves string, id int) (int, bool) { + rc.mu.RLock() + defer rc.mu.RUnlock() + + v, ok := rc.data[moves][id] + return v, ok +} + +func (rc *robotCache) contains(moves string) bool { + rc.mu.RLock() + defer rc.mu.RUnlock() + + _, ok := rc.data[moves] + return ok +} + +func (rc *robotCache) insert(moves string, id, val int) { + rc.mu.Lock() + defer rc.mu.Unlock() + + if _, ok := rc.data[moves]; !ok { + rc.data[moves] = make(map[int]int) + } + rc.data[moves][id] = val +} diff --git a/day-21/internal/two/solve.go b/day-21/internal/two/solve.go new file mode 100644 index 0000000..92f77f2 --- /dev/null +++ b/day-21/internal/two/solve.go @@ -0,0 +1,71 @@ +package two + +import ( + "bytes" + "strings" + + "github.com/onyx-and-iris/aoc2024/day-21/internal/pad" +) + +func Solve(buf []byte) (int, error) { + r := bytes.NewReader(buf) + codes, err := parseLines(r) + if err != nil { + return 0, err + } + + complexityChan := make(chan int) + conc := len(codes) + + for _, code := range codes { + go func() { + numpad := pad.NewNumpad() + dirpad := pad.NewDirpad() + complexityChan <- numFromCode(code) * generateIntFromMoves(numpad, dirpad, code, 25) + }() + } + + var complexity int + for range conc { + complexity += <-complexityChan + } + + return complexity, nil +} + +type generator interface { + Generate(code string) string +} + +func generateIntFromMoves(n, d generator, code string, numRobots int) int { + moves := n.Generate(code) + return recurseForRobots(d, moves, numRobots, 1, newRobotCache()) +} + +func recurseForRobots( + d generator, + moves string, + numRobots int, + id int, + memo robotCache, +) int { + if memo.contains(moves) { + if v, ok := memo.read(moves, id-1); ok { + return v + } + } + + next := d.Generate(moves) + if id == numRobots { + return len(next) + } + + var totalCount int + for _, move := range strings.SplitAfter(next, "A") { + count := recurseForRobots(d, move, numRobots, id+1, memo) + totalCount += count + } + memo.insert(moves, id-1, totalCount) + + return totalCount +} diff --git a/day-21/internal/two/solve_internal_test.go b/day-21/internal/two/solve_internal_test.go new file mode 100644 index 0000000..ecb05d5 --- /dev/null +++ b/day-21/internal/two/solve_internal_test.go @@ -0,0 +1,15 @@ +package two + +import ( + _ "embed" + "os" + "testing" +) + +//go:embed testdata/input.txt +var data []byte + +func BenchmarkSolve(b *testing.B) { + os.Stdout, _ = os.Open(os.DevNull) + Solve(data) +} diff --git a/day-21/internal/two/util.go b/day-21/internal/two/util.go new file mode 100644 index 0000000..63698f9 --- /dev/null +++ b/day-21/internal/two/util.go @@ -0,0 +1,27 @@ +package two + +import ( + "bufio" + "io" + "strconv" +) + +func parseLines(r io.Reader) ([]string, error) { + codes := []string{} + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + codes = append(codes, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return codes, nil +} + +func numFromCode(s string) int { + n, _ := strconv.Atoi(s[:len(s)-1]) + return n +} diff --git a/day-21/makefile b/day-21/makefile new file mode 100644 index 0000000..98796ed --- /dev/null +++ b/day-21/makefile @@ -0,0 +1,30 @@ +program = day-21 + +GO = go +SRC_DIR := src +BIN_DIR := bin + +EXE := $(BIN_DIR)/$(program) + +.DEFAULT_GOAL := build + +.PHONY: fmt vet build bench clean +fmt: + $(GO) fmt ./... + +vet: fmt + $(GO) vet ./... + +build: vet | $(BIN_DIR) + $(GO) build -o $(EXE) ./$(SRC_DIR) + +bench: + $(GO) test ./internal/one/ -bench=. > internal/one/benchmark + $(GO) test ./internal/two/ -bench=. > internal/two/benchmark + $(GO) test . -count=10 -bench=. > benchmark + +$(BIN_DIR): + @mkdir -p $@ + +clean: + @rm -rv $(BIN_DIR) diff --git a/day-21/solve.go b/day-21/solve.go new file mode 100644 index 0000000..3e4e84f --- /dev/null +++ b/day-21/solve.go @@ -0,0 +1,20 @@ +package daytwentyone + +import ( + "github.com/onyx-and-iris/aoc2024/day-21/internal/one" + "github.com/onyx-and-iris/aoc2024/day-21/internal/two" +) + +func Solve(buf []byte) (int, int, error) { + answerOne, err := one.Solve(buf) + if err != nil { + return 0, 0, err + } + + answerTwo, err := two.Solve(buf) + if err != nil { + return 0, 0, err + } + + return answerOne, answerTwo, nil +} diff --git a/day-21/solve_internal_test.go b/day-21/solve_internal_test.go new file mode 100644 index 0000000..6c2b2ca --- /dev/null +++ b/day-21/solve_internal_test.go @@ -0,0 +1,15 @@ +package daytwentyone + +import ( + _ "embed" + "os" + "testing" +) + +//go:embed testdata/input.txt +var data []byte + +func BenchmarkSolve(b *testing.B) { + os.Stdout, _ = os.Open(os.DevNull) + Solve(data) +}