From 72d329ae577a109bcf0c24b33e7f234ebbcb3fe3 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Fri, 20 Dec 2024 20:48:13 +0000 Subject: [PATCH] add day-20 --- day-20/cmd/cli/main.go | 41 +++++++++++ day-20/go.mod | 10 +++ day-20/go.sum | 18 +++++ day-20/internal/one/graph.go | 86 ++++++++++++++++++++++ day-20/internal/one/neighbours.go | 21 ++++++ day-20/internal/one/solve.go | 72 ++++++++++++++++++ day-20/internal/one/solve_internal_test.go | 15 ++++ day-20/internal/one/util.go | 44 +++++++++++ day-20/internal/point/point.go | 9 +++ day-20/internal/queue/queue.go | 39 ++++++++++ day-20/internal/two/graph.go | 38 ++++++++++ day-20/internal/two/solve.go | 38 ++++++++++ day-20/internal/two/solve_internal_test.go | 15 ++++ day-20/internal/two/util.go | 44 +++++++++++ day-20/makefile | 30 ++++++++ day-20/solve.go | 20 +++++ day-20/solve_internal_test.go | 15 ++++ 17 files changed, 555 insertions(+) create mode 100644 day-20/cmd/cli/main.go create mode 100644 day-20/go.mod create mode 100644 day-20/go.sum create mode 100644 day-20/internal/one/graph.go create mode 100644 day-20/internal/one/neighbours.go create mode 100644 day-20/internal/one/solve.go create mode 100644 day-20/internal/one/solve_internal_test.go create mode 100644 day-20/internal/one/util.go create mode 100644 day-20/internal/point/point.go create mode 100644 day-20/internal/queue/queue.go create mode 100644 day-20/internal/two/graph.go create mode 100644 day-20/internal/two/solve.go create mode 100644 day-20/internal/two/solve_internal_test.go create mode 100644 day-20/internal/two/util.go create mode 100644 day-20/makefile create mode 100644 day-20/solve.go create mode 100644 day-20/solve_internal_test.go diff --git a/day-20/cmd/cli/main.go b/day-20/cmd/cli/main.go new file mode 100644 index 0000000..ea847c4 --- /dev/null +++ b/day-20/cmd/cli/main.go @@ -0,0 +1,41 @@ +/******************************************************************************** + Advent of Code 2024 - day-20 +********************************************************************************/ + +package main + +import ( + "embed" + "flag" + "fmt" + "slices" + + log "github.com/sirupsen/logrus" + + problems "github.com/onyx-and-iris/aoc2024/day-20" +) + +//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-20/go.mod b/day-20/go.mod new file mode 100644 index 0000000..f871790 --- /dev/null +++ b/day-20/go.mod @@ -0,0 +1,10 @@ +module github.com/onyx-and-iris/aoc2024/day-20 + +go 1.23.3 + +require ( + github.com/elliotchance/orderedmap/v3 v3.0.0 + github.com/sirupsen/logrus v1.9.3 +) + +require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect diff --git a/day-20/go.sum b/day-20/go.sum new file mode 100644 index 0000000..a96b95f --- /dev/null +++ b/day-20/go.sum @@ -0,0 +1,18 @@ +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/elliotchance/orderedmap/v3 v3.0.0 h1:Yay/tDjX+vzza+Drcoo8VEbuBnOYGpgenCXWcpQSFDg= +github.com/elliotchance/orderedmap/v3 v3.0.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= +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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/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-20/internal/one/graph.go b/day-20/internal/one/graph.go new file mode 100644 index 0000000..86a52e7 --- /dev/null +++ b/day-20/internal/one/graph.go @@ -0,0 +1,86 @@ +package one + +import ( + "slices" + "strings" + + "github.com/onyx-and-iris/aoc2024/day-20/internal/point" + + "github.com/elliotchance/orderedmap/v3" +) + +type graph struct { + start, end point.Point + data []string +} + +func newGraph(data []string) *graph { + return &graph{data: data} +} + +func (g *graph) String() string { + return strings.Join(g.data, "\n") +} + +func (g *graph) isOutOfBounds(p point.Point) bool { + return p.X < 1 || p.Y < 1 || p.Y >= len(g.data)-1 || p.X >= len(g.data[p.Y])-1 +} + +func (g *graph) valueAt(p point.Point) rune { + return rune(g.data[p.Y][p.X]) +} + +func (g *graph) isValidCheatPosition(p point.Point) bool { + allowed := []rune{'.', 'S', 'E'} + + ns := neighbours(p) + + return slices.Contains(allowed, g.valueAt(ns[N])) && slices.Contains(allowed, g.valueAt(ns[S])) || + slices.Contains(allowed, g.valueAt(ns[W])) && slices.Contains(allowed, g.valueAt(ns[E])) +} + +func (g *graph) getConnectingpoint(dir direction, p point.Point) point.Point { + ns := neighbours(p) + + return ns[dir] +} + +func (g *graph) set(p point.Point, value rune) { + g.data[p.Y] = replaceAtIndex(g.data[p.Y], value, p.X) +} + +func (g *graph) path( + current point.Point, + om *orderedmap.OrderedMap[point.Point, int], + dist int, +) *orderedmap.OrderedMap[point.Point, int] { + if current == g.end { + return om + } + + var n point.Point + for _, n = range neighbours(current) { + _, ok := om.Get(n) + if ok { + continue + } + + if g.valueAt(n) == '.' || g.valueAt(n) == 'E' { + break + } + } + om.Set(n, dist) + + return g.path(n, om, dist+1) +} + +func (g *graph) debug(path *orderedmap.OrderedMap[point.Point, int]) string { + temp := slices.Clone(g.data) + for n := range path.Keys() { + if g.valueAt(n) == cheatChar { + continue + } + temp[n.Y] = replaceAtIndex(temp[n.Y], 'O', n.X) + } + return strings.Join(temp, "\n") +} diff --git a/day-20/internal/one/neighbours.go b/day-20/internal/one/neighbours.go new file mode 100644 index 0000000..2d0bbd5 --- /dev/null +++ b/day-20/internal/one/neighbours.go @@ -0,0 +1,21 @@ +package one + +import "github.com/onyx-and-iris/aoc2024/day-20/internal/point" + +type direction int + +const ( + N direction = iota + E + S + W +) + +func neighbours(p point.Point) [4]point.Point { + return [4]point.Point{ + point.New(p.X, p.Y-1), // N + point.New(p.X+1, p.Y), // E + point.New(p.X, p.Y+1), // S + point.New(p.X-1, p.Y), // W + } +} diff --git a/day-20/internal/one/solve.go b/day-20/internal/one/solve.go new file mode 100644 index 0000000..5691396 --- /dev/null +++ b/day-20/internal/one/solve.go @@ -0,0 +1,72 @@ +package one + +import ( + "bytes" + "math" + + "github.com/onyx-and-iris/aoc2024/day-20/internal/point" + log "github.com/sirupsen/logrus" + + "github.com/elliotchance/orderedmap/v3" +) + +const cheatChar = 'X' + +var Path *orderedmap.OrderedMap[point.Point, int] + +func Solve(buf []byte) (int, error) { + r := bytes.NewReader(buf) + graph, err := parseLines(r) + if err != nil { + return 0, err + } + + Path = orderedmap.NewOrderedMap[point.Point, int]() + Path.Set(graph.start, 0) + Path = graph.path(graph.start, Path, 1) + log.Debugf("initial path: %d\n", Path.Len()-1) + log.Debugf("Unique path:\n%s\n", graph.debug(Path)) + + visited := make(map[point.Point]struct{}) + + var sum int + for point := range Path.Keys() { + for dir, n := range neighbours(point) { + if graph.isOutOfBounds(n) { + continue + } + + if graph.valueAt(n) != '#' { + continue + } + + if _, ok := visited[n]; ok { + continue + } + + if graph.isValidCheatPosition(n) { + b := graph.getConnectingpoint(direction(dir), n) + graph.set(n, 'X') + + v1, ok := Path.Get(point) + if !ok { + log.Fatalf("%v not in path", point) + } + v2, ok := Path.Get(b) + if !ok { + log.Fatalf("%v not in path", b) + } + diff := int(math.Abs(float64(v1-v2))) - 1 + + log.Debugf("diff: %d\n", diff) + if diff >= 100 { + sum++ + } + + visited[n] = struct{}{} + } + } + } + + return sum, nil +} diff --git a/day-20/internal/one/solve_internal_test.go b/day-20/internal/one/solve_internal_test.go new file mode 100644 index 0000000..e5fd7ea --- /dev/null +++ b/day-20/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-20/internal/one/util.go b/day-20/internal/one/util.go new file mode 100644 index 0000000..bfa13a0 --- /dev/null +++ b/day-20/internal/one/util.go @@ -0,0 +1,44 @@ +package one + +import ( + "bufio" + "io" + "strings" + + "github.com/onyx-and-iris/aoc2024/day-20/internal/point" +) + +func parseLines(r io.Reader) (*graph, error) { + graph := newGraph(make([]string, 0)) + + var linecount int + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + + if strings.Contains(line, "S") { + indx := strings.Index(line, "S") + graph.start = point.New(indx, linecount) + } + if strings.Contains(line, "E") { + indx := strings.Index(line, "E") + graph.end = point.New(indx, linecount) + } + + graph.data = append(graph.data, line) + + linecount++ + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return graph, nil +} + +func replaceAtIndex(s string, r rune, i int) string { + out := []rune(s) + out[i] = r + return string(out) +} diff --git a/day-20/internal/point/point.go b/day-20/internal/point/point.go new file mode 100644 index 0000000..0253983 --- /dev/null +++ b/day-20/internal/point/point.go @@ -0,0 +1,9 @@ +package point + +type Point struct { + X, Y int +} + +func New(x, y int) Point { + return Point{x, y} +} diff --git a/day-20/internal/queue/queue.go b/day-20/internal/queue/queue.go new file mode 100644 index 0000000..b38c5f3 --- /dev/null +++ b/day-20/internal/queue/queue.go @@ -0,0 +1,39 @@ +package queue + +import "sync" + +type ConcurrentQueue[T comparable] struct { + items []T + lock sync.Mutex + cond *sync.Cond +} + +func New[T comparable]() *ConcurrentQueue[T] { + q := &ConcurrentQueue[T]{} + q.cond = sync.NewCond(&q.lock) + return q +} + +func (q *ConcurrentQueue[T]) Enqueue(item T) { + q.lock.Lock() + defer q.lock.Unlock() + + q.items = append(q.items, item) + q.cond.Signal() +} + +func (q *ConcurrentQueue[T]) Dequeue() T { + q.lock.Lock() + defer q.lock.Unlock() + + for len(q.items) == 0 { + q.cond.Wait() + } + item := q.items[0] + q.items = q.items[1:] + return item +} + +func (q *ConcurrentQueue[T]) IsEmpty() bool { + return len(q.items) == 0 +} diff --git a/day-20/internal/two/graph.go b/day-20/internal/two/graph.go new file mode 100644 index 0000000..17cd3f0 --- /dev/null +++ b/day-20/internal/two/graph.go @@ -0,0 +1,38 @@ +package two + +import ( + "slices" + "strings" + + "github.com/onyx-and-iris/aoc2024/day-20/internal/point" + + "github.com/elliotchance/orderedmap/v3" +) + +type graph struct { + start, end point.Point + data []string +} + +func newGraph(data []string) *graph { + return &graph{data: data} +} + +func (g *graph) String() string { + return strings.Join(g.data, "\n") +} + +func (g *graph) valueAt(p point.Point) rune { + return rune(g.data[p.Y][p.X]) +} + +func (g *graph) debug(path *orderedmap.OrderedMap[point.Point, int]) string { + temp := slices.Clone(g.data) + for n := range path.Keys() { + if g.valueAt(n) == 'X' { + continue + } + temp[n.Y] = replaceAtIndex(temp[n.Y], 'O', n.X) + } + return strings.Join(temp, "\n") +} diff --git a/day-20/internal/two/solve.go b/day-20/internal/two/solve.go new file mode 100644 index 0000000..3a4a31d --- /dev/null +++ b/day-20/internal/two/solve.go @@ -0,0 +1,38 @@ +package two + +import ( + "bytes" + "math" + + "github.com/onyx-and-iris/aoc2024/day-20/internal/one" + "github.com/onyx-and-iris/aoc2024/day-20/internal/point" + log "github.com/sirupsen/logrus" +) + +func Solve(buf []byte) (int, error) { + r := bytes.NewReader(buf) + graph, err := parseLines(r) + if err != nil { + return 0, err + } + + log.Debugf("\n%s\n", graph.debug(one.Path)) + + var count int + for pair1 := one.Path.Front(); pair1 != nil; pair1 = pair1.Next() { + for pair2 := one.Path.Front(); pair2 != nil; pair2 = pair2.Next() { + if manhatten(pair1.Key, pair2.Key) > 20 || + pair1.Value-pair2.Value-manhatten(pair1.Key, pair2.Key) < 100 { + continue + } + + count++ + } + } + + return count, nil +} + +func manhatten(a, b point.Point) int { + return int(math.Abs(float64(a.X)-float64(b.X)) + math.Abs(float64(a.Y)-float64(b.Y))) +} diff --git a/day-20/internal/two/solve_internal_test.go b/day-20/internal/two/solve_internal_test.go new file mode 100644 index 0000000..ecb05d5 --- /dev/null +++ b/day-20/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-20/internal/two/util.go b/day-20/internal/two/util.go new file mode 100644 index 0000000..f054005 --- /dev/null +++ b/day-20/internal/two/util.go @@ -0,0 +1,44 @@ +package two + +import ( + "bufio" + "io" + "strings" + + "github.com/onyx-and-iris/aoc2024/day-20/internal/point" +) + +func parseLines(r io.Reader) (*graph, error) { + graph := newGraph(make([]string, 0)) + + var linecount int + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + + if strings.Contains(line, "S") { + indx := strings.Index(line, "S") + graph.start = point.New(indx, linecount) + } + if strings.Contains(line, "E") { + indx := strings.Index(line, "E") + graph.end = point.New(indx, linecount) + } + + graph.data = append(graph.data, line) + + linecount++ + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return graph, nil +} + +func replaceAtIndex(s string, r rune, i int) string { + out := []rune(s) + out[i] = r + return string(out) +} diff --git a/day-20/makefile b/day-20/makefile new file mode 100644 index 0000000..5c3fe6d --- /dev/null +++ b/day-20/makefile @@ -0,0 +1,30 @@ +program = day-20 + +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-20/solve.go b/day-20/solve.go new file mode 100644 index 0000000..c5279de --- /dev/null +++ b/day-20/solve.go @@ -0,0 +1,20 @@ +package daytwenty + +import ( + "github.com/onyx-and-iris/aoc2024/day-20/internal/one" + "github.com/onyx-and-iris/aoc2024/day-20/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-20/solve_internal_test.go b/day-20/solve_internal_test.go new file mode 100644 index 0000000..350c0d2 --- /dev/null +++ b/day-20/solve_internal_test.go @@ -0,0 +1,15 @@ +package daytwenty + +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) +}