From 497da642b2475f7ecb11cda712efc7e088242c95 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Fri, 22 Dec 2023 21:56:07 +0000 Subject: [PATCH] day-17 --- day-17/go.mod | 3 ++ day-17/makefile | 10 ++++ day-17/node.go | 32 ++++++++++++ day-17/one.go | 114 ++++++++++++++++++++++++++++++++++++++++++ day-17/pqueue.go | 49 ++++++++++++++++++ day-17/pqueue_test.go | 27 ++++++++++ day-17/solution.go | 21 ++++++++ day-17/two.go | 14 ++++++ day-17/two_test.go | 26 ++++++++++ day-17/util.go | 38 ++++++++++++++ 10 files changed, 334 insertions(+) create mode 100644 day-17/go.mod create mode 100644 day-17/makefile create mode 100644 day-17/node.go create mode 100644 day-17/one.go create mode 100644 day-17/pqueue.go create mode 100644 day-17/pqueue_test.go create mode 100644 day-17/solution.go create mode 100644 day-17/two.go create mode 100644 day-17/two_test.go create mode 100644 day-17/util.go diff --git a/day-17/go.mod b/day-17/go.mod new file mode 100644 index 0000000..0c11699 --- /dev/null +++ b/day-17/go.mod @@ -0,0 +1,3 @@ +module github.com/onyx-and-iris/aoc2023/day-17 + +go 1.21.5 diff --git a/day-17/makefile b/day-17/makefile new file mode 100644 index 0000000..d1bbae3 --- /dev/null +++ b/day-17/makefile @@ -0,0 +1,10 @@ +TEST="test.txt" +INPUT="input.txt" + +test: + go run . < $(TEST) + +run: + go run . < $(INPUT) + +all: test diff --git a/day-17/node.go b/day-17/node.go new file mode 100644 index 0000000..61fd66c --- /dev/null +++ b/day-17/node.go @@ -0,0 +1,32 @@ +package main + +import "fmt" + +type coords struct { + X int + Y int +} + +func newCoords(x, y int) coords { + return coords{X: x, Y: y} +} + +// node represents a single point on the graph +type node struct { + cost int + distance int + directionX int + directionY int + coords + index int +} + +func newNode(cost, distance, directionX, directionY, x, y int) *node { + c := newCoords(x, y) + return &node{cost: cost, distance: distance, directionX: directionX, directionY: directionY, coords: c} +} + +// String implements the fmt.Stringer interface +func (n node) String() string { + return fmt.Sprintf("%d%d%d%v", n.distance, n.directionX, n.directionY, n.coords) +} diff --git a/day-17/one.go b/day-17/one.go new file mode 100644 index 0000000..89f7dd7 --- /dev/null +++ b/day-17/one.go @@ -0,0 +1,114 @@ +package main + +import ( + "container/heap" + + log "github.com/sirupsen/logrus" +) + +type option func(*dijkstra) + +func WithMinDistance(distance int) option { + return func(d *dijkstra) { + d.minDistance = distance + } +} + +func WithMaxDistance(distance int) option { + return func(d *dijkstra) { + d.maxDistance = distance + } +} + +type dijkstra struct { + graph [][]int + minDistance int + maxDistance int +} + +func newDijkstra(graph [][]int, opts ...option) *dijkstra { + d := &dijkstra{graph: graph} + + for _, opt := range opts { + opt(d) + } + + return d +} + +func (d dijkstra) initialize(start coords) *pqueue { + pq := newPriorityQueue() + heap.Init(pq) + // we don't encounter heat loss for start point unless we enter this block again + heap.Push(pq, newNode(0, 0, 0, 0, start.X, start.Y)) + return pq +} + +// run performs the lowest cost dijkstra algorithm with a min heap +func (d dijkstra) run(start, end coords) int { + pq := d.initialize(start) + + visited := map[string]bool{} + + for pq.Len() > 0 { + cost, node := func() (int, *node) { + x := heap.Pop(pq).(*node) + return x.cost, x + }() + + // we reached final location, return its lowest cost + if node.X == end.X && node.Y == end.Y && node.distance >= d.minDistance { + log.Debug("returning lowest cost with min distance >= ", d.minDistance) + return node.cost + } + + if _, ok := visited[node.String()]; ok { + continue + } + visited[node.String()] = true + + var neighbours = [][]int{{0, -1}, {0, 1}, {-1, 0}, {1, 0}} // N, S, W, E + for _, n := range neighbours { + nextX := node.X + n[0] + nextY := node.Y + n[1] + + if nextY < 0 || nextY >= len(d.graph) || nextX < 0 || nextX >= len(d.graph[nextY]) { + continue + } + + if node.directionX == -n[0] && node.directionY == -n[1] { // are we going backwards? + continue + } + + var distance = 1 + if node.directionX == n[0] || node.directionY == n[1] { // same direction + distance = node.distance + 1 + } else { + if node.distance < d.minDistance { + continue + } + } + + if distance > d.maxDistance { + continue + } + + new_cost := cost + d.graph[nextY][nextX] + heap.Push(pq, newNode(new_cost, distance, n[0], n[1], nextX, nextY)) + } + } + + return 0 +} + +// one returns the lowest cost path from start to end +func one(lines []string) int { + graph := buildGraph(lines) + + start := newCoords(0, 0) + end := newCoords(len(graph[0])-1, len(graph)-1) + dijkstra := newDijkstra(graph, WithMaxDistance(3)) + cost := dijkstra.run(start, end) + + return cost +} diff --git a/day-17/pqueue.go b/day-17/pqueue.go new file mode 100644 index 0000000..1040b14 --- /dev/null +++ b/day-17/pqueue.go @@ -0,0 +1,49 @@ +package main + +import ( + "container/heap" +) + +// pqueue implements the heap.Interface interface +// it represents a min heap priority queue +type pqueue []*node + +func newPriorityQueue() *pqueue { + pq := make(pqueue, 0) + return &pq +} + +func (pq pqueue) Len() int { + return len(pq) +} + +func (pq *pqueue) Push(x interface{}) { + n := len(*pq) + node := x.(*node) + node.index = n + *pq = append(*pq, node) +} + +func (pq *pqueue) Pop() interface{} { + old := *pq + n := len(old) + node := old[n-1] + node.index = -1 + *pq = old[0 : n-1] + return node +} + +func (pq *pqueue) Update(node *node, value int) { + node.cost = value + heap.Fix(pq, node.index) +} + +func (pq pqueue) Less(i, j int) bool { + return pq[i].cost < pq[j].cost +} + +func (pq pqueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] + pq[i].index = i + pq[j].index = j +} diff --git a/day-17/pqueue_test.go b/day-17/pqueue_test.go new file mode 100644 index 0000000..7af4ece --- /dev/null +++ b/day-17/pqueue_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "container/heap" + "testing" + + "github.com/go-playground/assert/v2" +) + +func TestPriorityQueue(t *testing.T) { + //t.Skip("skipping test") + pq := newPriorityQueue() + + heap.Push(pq, newNode(30, 0, 0, 0, 0, 0)) + heap.Push(pq, newNode(10, 0, 0, 0, 8, 0)) + heap.Push(pq, newNode(20, 0, 0, 0, 13, 0)) + + t.Run("Should create a queue size 3", func(t *testing.T) { + assert.Equal(t, 3, pq.Len()) + }) + + item := heap.Pop(pq).(*node) + + t.Run("Should return item with cost 10", func(t *testing.T) { + assert.Equal(t, 10, item.cost) + }) +} diff --git a/day-17/solution.go b/day-17/solution.go new file mode 100644 index 0000000..926f1d4 --- /dev/null +++ b/day-17/solution.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + + log "github.com/sirupsen/logrus" +) + +func init() { + log.SetLevel(log.InfoLevel) +} + +func main() { + lines := readlines() + + ans := one(lines) + fmt.Printf("solution one: %d\n", ans) + + ans = two(lines) + fmt.Printf("solution two: %d\n", ans) +} diff --git a/day-17/two.go b/day-17/two.go new file mode 100644 index 0000000..1033134 --- /dev/null +++ b/day-17/two.go @@ -0,0 +1,14 @@ +package main + +// two returns the lowest cost path from start to end +// with a min/max distance set +func two(lines []string) int { + graph := buildGraph(lines) + + start := newCoords(0, 0) + end := newCoords(len(graph[0])-1, len(graph)-1) + dijkstra := newDijkstra(graph, WithMinDistance(4), WithMaxDistance(10)) + cost := dijkstra.run(start, end) + + return cost +} diff --git a/day-17/two_test.go b/day-17/two_test.go new file mode 100644 index 0000000..2ecb8c1 --- /dev/null +++ b/day-17/two_test.go @@ -0,0 +1,26 @@ +package main + +import ( + _ "embed" + "strings" + + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + //go:embed test2.txt + testInput2 []byte +) + +func TestDjistraWithMinDistance(t *testing.T) { + //t.Skip("skipping test") + + input := strings.Split(string(testInput2), "\n") + cost := two(input) + + t.Run("Should return a lowest cost of 71", func(t *testing.T) { + assert.Equal(t, 71, cost) + }) +} diff --git a/day-17/util.go b/day-17/util.go new file mode 100644 index 0000000..d70d538 --- /dev/null +++ b/day-17/util.go @@ -0,0 +1,38 @@ +package main + +import ( + "bufio" + "log" + "os" +) + +// readlines reads lines from stdin. +// it returns them as an array of strings +func readlines() []string { + lines := []string{} + + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + + return lines +} + +// buildGraph parses lines into costs for graph +func buildGraph(lines []string) [][]int { + graph := make([][]int, len(lines)) + + for i, line := range lines { + graph[i] = make([]int, len(line)) + for j, r := range line { + graph[i][j] = int(r - '0') + } + } + + return graph +}