diff --git a/day16/dijsktra.go.disabled b/day16/dijsktra.go.disabled new file mode 100644 index 0000000..14e045d --- /dev/null +++ b/day16/dijsktra.go.disabled @@ -0,0 +1,220 @@ +package main + +import ( + "fmt" + "slices" + + "git.mstar.dev/mstar/aoc24/util" + "git.mstar.dev/mstar/goutils/sliceutils" + "github.com/RyanCarrier/dijkstra/v2" +) + +type NodeType int + +// A crossing is a path location with 3 or 4 other paths next to it +type Node struct { + At util.Vec2 + Type NodeType + Dirs []util.Vec2 + Index int +} + +const ( + NodeInvalid NodeType = iota + NodeCrossing + NodeCorner + NodeDeadEnd +) + +var ( + DirUp = util.Vec2{X: 0, Y: -1} + DirRight = util.Vec2{X: 1, Y: 0} + DirDown = util.Vec2{X: 0, Y: 1} + DirLeft = util.Vec2{X: -1, Y: 0} +) + +var startDir = DirRight + +// Get the size of the bord +func getSize(lines []string) util.Vec2 { + return util.Vec2{X: int64(len(lines[0])), Y: int64(len(lines))} +} + +// Count the paths next to a given positon +func countPathsAt(lines [][]rune, at util.Vec2) []util.Vec2 { + center := lines[at.Y][at.X] + paths := []util.Vec2{} + if center == '#' { + return paths + } + if util.SafeGet(lines, at.Up()) != '#' { + paths = append(paths, DirUp) + } + if util.SafeGet(lines, at.Down()) != '#' { + paths = append(paths, DirDown) + } + if util.SafeGet(lines, at.Left()) != '#' { + paths = append(paths, DirLeft) + } + if util.SafeGet(lines, at.Right()) != '#' { + paths = append(paths, DirRight) + } + return paths +} + +func getCrossingsAndCornerPositions(lines [][]rune) []Node { + out := []Node{} + + for iy, line := range lines { + for ix, char := range line { + if char == '#' { + continue + } + pos := util.Vec2{X: int64(ix), Y: int64(iy)} + dirs := countPathsAt(lines, pos) + // Crossings have 3 or more paths out + if len(dirs) >= 3 { + out = append(out, Node{pos, NodeCrossing, dirs, len(out)}) + } + // Also include dead ends + if len(dirs) == 1 { + out = append(out, Node{pos, NodeDeadEnd, dirs, len(out)}) + } + // Location is a corner if the paths are not opposite each other + if len(dirs) == 2 { + if !dirs[0].Mult(-1).Eq(dirs[1]) { + out = append(out, Node{pos, NodeCorner, dirs, len(out)}) + } + } + } + } + return out +} + +func getNeighbourNodes(nodes []Node, index int) []int { + target := nodes[index] + + potential := sliceutils.Filter(nodes, func(t Node) bool { + return t.Index != index && (t.At.X == target.At.X || t.At.Y == target.At.Y) + }) + + hits := []Node{} + for _, dir := range target.Dirs { + switch dir { + case DirUp: + hits = append(hits, slices.MinFunc(potential, func(a, b Node) int { + if a.At.X != target.At.X || a.At.Y > target.At.Y { + return -1 + } + if b.At.X != target.At.X || b.At.Y > target.At.Y { + return 1 + } + return util.AbsI( + int(target.At.Y)-int(a.At.Y), + ) - util.AbsI( + int(target.At.Y)-int(b.At.Y), + ) + })) + case DirDown: + hits = append(hits, slices.MinFunc(potential, func(a, b Node) int { + if a.At.X != target.At.X || a.At.Y < target.At.Y { + return -1 + } + if b.At.X != target.At.X || b.At.Y < target.At.Y { + return 1 + } + return util.AbsI( + int(target.At.Y)-int(a.At.Y), + ) - util.AbsI( + int(target.At.Y)-int(b.At.Y), + ) + })) + case DirLeft: + hits = append(hits, slices.MinFunc(potential, func(a, b Node) int { + if a.At.Y != target.At.Y || a.At.X > target.At.X { + return -1 + } + if b.At.Y != target.At.Y || b.At.X > target.At.X { + return 1 + } + return util.AbsI( + int(target.At.X)-int(a.At.X), + ) - util.AbsI( + int(target.At.X)-int(b.At.X), + ) + })) + case DirRight: + hits = append(hits, slices.MinFunc(potential, func(a, b Node) int { + if a.At.Y != target.At.Y || a.At.X > target.At.X { + return -1 + } + if b.At.Y != target.At.Y || b.At.X > target.At.X { + return 1 + } + return util.AbsI( + int(target.At.X)-int(a.At.X), + ) - util.AbsI( + int(target.At.X)-int(b.At.X), + ) + })) + default: + panic("Unknown dir") + } + } + return sliceutils.Map(hits, func(t Node) int { return t.Index }) +} + +func addNodesToGraph(nodes []Node, graph *dijkstra.Graph) { + for i := range len(nodes) { + if err := graph.AddEmptyVertex(i); err != nil { + panic(err) + } + } + for _, node := range nodes { + for _, n := range getNeighbourNodes(nodes, node.Index) { + if err := graph.AddArc(node.Index, n, node.At.Add(nodes[n].At.Mult(-1)).LenSquared()); err != nil { + panic(err) + } + } + } +} + +func findEnd(lines [][]rune) util.Vec2 { + for iy, line := range lines { + for ix, char := range line { + if char == 'E' { + return util.Vec2{X: int64(ix), Y: int64(iy)} + } + } + } + return util.Vec2{X: -1, Y: -1} +} + +func findStart(lines [][]rune) util.Vec2 { + for iy, line := range lines { + for ix, char := range line { + if char == 'S' { + return util.Vec2{X: int64(ix), Y: int64(iy)} + } + } + } + return util.Vec2{X: -1, Y: -1} +} + +func main() { + inputLines := util.FileContentToNonEmptyLines(util.LoadFileFromArgs()) + // size := getSize(inputLines) + inputLineChars := sliceutils.Map(inputLines, func(t string) []rune { return []rune(t) }) + nodes := getCrossingsAndCornerPositions(inputLineChars) + // fmt.Println(nodes) + // slices.MaxFunc(x S, cmp func(a E, b E) int) + graph := dijkstra.NewGraph() + addNodesToGraph(nodes, &graph) + startVec := findStart(inputLineChars) + endVec := findEnd(inputLineChars) + startI := slices.IndexFunc(nodes, func(e Node) bool { return e.At == startVec }) + endI := slices.IndexFunc(nodes, func(e Node) bool { return e.At == endVec }) + + path, err := graph.Shortest(startI, endI) + fmt.Println(path, err) +} diff --git a/day16/main.go b/day16/main.go index 739267a..04aa201 100644 --- a/day16/main.go +++ b/day16/main.go @@ -1,7 +1,10 @@ package main import ( + "fmt" "math" + "slices" + "strings" "git.mstar.dev/mstar/goutils/sliceutils" @@ -12,7 +15,14 @@ import ( type Crossing struct { At util.Vec2 LowestScore int - LowestFrom int // Index into list of crossings + LowestFrom util.Vec2 // Index into list of crossings + Neighbours []util.Vec2 // Directions where a path is +} + +type Area map[int64]map[int64]*Crossing + +type Wrapper struct { + A Area } var ( @@ -30,52 +40,50 @@ func getSize(lines []string) util.Vec2 { } // Count the paths next to a given positon -func countPathsAt(lines [][]rune, at util.Vec2) int { +func countPathsAt(lines [][]rune, at util.Vec2) []util.Vec2 { center := lines[at.Y][at.X] + paths := []util.Vec2{} if center == '#' { - return 0 + return paths } - if center == 'E' { - return 1 - } - if center == 'S' { - return 1 - } - acc := 0 if util.SafeGet(lines, at.Up()) != '#' { - acc++ + paths = append(paths, at.Up()) } if util.SafeGet(lines, at.Down()) != '#' { - acc++ + paths = append(paths, at.Down()) } if util.SafeGet(lines, at.Left()) != '#' { - acc++ + paths = append(paths, at.Left()) } if util.SafeGet(lines, at.Right()) != '#' { - acc++ + paths = append(paths, at.Right()) } - return acc + return paths } -func findAllCrossings(lines [][]rune) []Crossing { - out := []Crossing{} - +func linesToMap(lines [][]rune, size util.Vec2) Area { + area := Area{} + // Ensure area has full maps + for i := range size.Y { + area[i] = map[int64]*Crossing{} + } for iy, line := range lines { - for ix := range line { - vec := util.Vec2{X: int64(ix), Y: int64(iy)} - pathCount := countPathsAt(lines, vec) - if pathCount >= 3 { - out = append(out, Crossing{At: vec, LowestScore: math.MaxInt, LowestFrom: -1}) + for ix, char := range line { + pos := util.Vec2{X: int64(ix), Y: int64(iy)} + switch char { + case '.', 'E', 'S': + dirs := countPathsAt(lines, pos) + area[int64(ix)][int64(iy)] = &Crossing{ + At: pos, + LowestScore: math.MaxInt64, + LowestFrom: util.Vec2{X: -1, Y: -1}, + Neighbours: dirs, + } + default: } } } - - return out -} - -// Convert a list of crossings into x y map of pointers to those crossings -func crossingListToPointerMap(crossings []Crossing) map[int64]map[int64]*Crossing { - return nil + return area } func findStart(lines [][]rune) util.Vec2 { @@ -102,28 +110,100 @@ func findEnd(lines [][]rune) util.Vec2 { return util.Vec2{X: -1, Y: -1} } -func walk(area [][]rune, from util.Vec2, dir util.Vec2) { +func walk( + area *Wrapper, + from util.Vec2, + dir util.Vec2, + score int, +) { // Idea: walk forward from start until crossing or wall // At crossing, defer a walker for both sides (if not walled off) but prioritise forwards - // Keep track of current score, one crossing = 1 point - // walking length doesn't matter because negligable compared to turn cost // At crossing, check if own score is lower than current score // If it is, update score and set own origin + // Else if crossing has a lower score than self, abort // Special case: // If at end, update end "crossing" following the earlier rules + + // Walk forward in dir until no node found + type WalkTarget struct { + Pos, Dir util.Vec2 + Score int + } + // targets := []WalkTarget{} + defer fmt.Println("Deffered done", from) + fmt.Println("Starting", from) + prev := from + for pos := from.Add(dir); area.A[pos.X][pos.Y] != nil; pos = pos.Add(dir) { + score++ + node := area.A[pos.X][pos.Y] + if node.LowestScore <= score { + return + } + node.LowestScore = score + node.LowestFrom = prev + // node.LowestFrom = pos.Add(dir.Mult(-1)) + fmt.Println("Setting node", pos, score, node.LowestFrom) + // Get all directions that don't match current walking direction (and reverse) + filtered := sliceutils.Filter(node.Neighbours, func(t util.Vec2) bool { + return !t.Add(pos.Mult(-1)).Eq(dir) && !t.Add(pos.Mult(-1)).Eq(dir.Mult(-1)) + }) + // fmt.Println("Filtered neighbours of node", filtered) + for _, neighbour := range filtered { + fmt.Println("Adding target", neighbour) + // targets = append(targets, WalkTarget{ + // neighbour, neighbour.Add(pos.Mult(-1)), score + 1001, + // }) + defer walk(area, neighbour, neighbour.Add(pos.Mult(-1)), score+1001) + } + fmt.Println("Stepping", dir) + prev = pos + } + // fmt.Println("Hitting stored targets", from) + // for _, target := range targets { + // walk(area, target.Pos, target.Dir, target.Score) + // } + fmt.Println("Done, walking deferred", from) +} + +func visualisePath(area Area, endPos, size util.Vec2, inputLineRunes [][]rune) string { + lineCopy := [][]rune{} + for _, line := range inputLineRunes { + lineCopy = append(lineCopy, slices.Clone(line)) + } + for pos := endPos; pos.IsInBounds(size); pos = area[pos.X][pos.Y].LowestFrom { + fmt.Println("Path elem", pos) + lineCopy[pos.Y][pos.X] = 'x' + } + builder := strings.Builder{} + for _, line := range lineCopy { + for _, char := range line { + builder.WriteRune(char) + } + builder.WriteRune('\n') + } + return builder.String() } func main() { inputLines := util.FileContentToNonEmptyLines(util.LoadFileFromArgs()) - // size := getSize(inputLines) - area := sliceutils.Map(inputLines, func(t string) []rune { return []rune(t) }) - crossings := findAllCrossings(area) + size := getSize(inputLines) + inputLineChars := sliceutils.Map(inputLines, func(t string) []rune { return []rune(t) }) + area := linesToMap(inputLineChars, size) - start := findStart(area) - // Start always has the lowest score - crossings = append(crossings, Crossing{At: start, LowestScore: math.MinInt, LowestFrom: -1}) - end := findEnd(area) - // While end is just another "crossing" - crossings = append(crossings, Crossing{At: end, LowestScore: math.MaxInt, LowestFrom: -1}) + start := findStart(inputLineChars) + end := findEnd(inputLineChars) + // Fill entire map + fmt.Println("Filling right") + wrapped := Wrapper{area} + walk(&wrapped, start, DirRight, 0) + fmt.Println("Filling up") + walk(&wrapped, start, DirUp, 1000) + fmt.Println("Filling down") + walk(&wrapped, start, DirDown, 1000) + fmt.Println("Filling left") + walk(&wrapped, start, DirLeft, 2000) + fmt.Printf("%#v\n", area[start.X][start.Y]) + fmt.Printf("Task 1: %d\n", area[end.X][end.Y].LowestScore) + fmt.Println(visualisePath(area, end, size, inputLineChars)) } diff --git a/go.mod b/go.mod index 16bac48..a19d5b8 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,8 @@ module git.mstar.dev/mstar/aoc24 go 1.23.3 require git.mstar.dev/mstar/goutils v1.5.4 + +require ( + github.com/RyanCarrier/dijkstra v1.4.0 // indirect + github.com/RyanCarrier/dijkstra/v2 v2.0.2 // indirect +) diff --git a/go.sum b/go.sum index 9638f4e..75fa651 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ git.mstar.dev/mstar/goutils v1.5.4 h1:l/4oQe/fBk9zyXplQkGXbmQndnm0aRdHuy4wgQfNrFo= git.mstar.dev/mstar/goutils v1.5.4/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= +github.com/RyanCarrier/dijkstra v1.4.0 h1:wkEVdTBLUiXjeBvDrSuagr72pOt36rUQoIfnnGxFYoQ= +github.com/RyanCarrier/dijkstra v1.4.0/go.mod h1:9egjhC7eVsfREX6NrYS+1wHzk9C/9v2Cz26/bqpjjTc= +github.com/RyanCarrier/dijkstra/v2 v2.0.2 h1:DIOg/a7XDR+KmlDkNSX9ggDY6sNLrG+EBGvZUjfgi+A= +github.com/RyanCarrier/dijkstra/v2 v2.0.2/go.mod h1:XwpYN7nC1LPwL3HkaavzB+VGaHRndSsZy/whsFy1AEI= diff --git a/util/vec.go b/util/vec.go index 9890aab..aff9ffc 100644 --- a/util/vec.go +++ b/util/vec.go @@ -31,3 +31,19 @@ func (v Vec2) Left() Vec2 { func (v Vec2) Right() Vec2 { return v.Add(Vec2{1, 0}) } + +func (v Vec2) RotateCounterClock() Vec2 { + return Vec2{v.Y, -v.X} +} + +func (v Vec2) RotateClock() Vec2 { + return Vec2{-v.Y, v.X} +} + +func (v Vec2) LenSquared() uint64 { + return uint64(v.X*v.X + v.Y*v.Y) +} + +func (v Vec2) IsInBounds(bounds Vec2) bool { + return v.X >= 0 && v.X < bounds.X && v.Y >= 0 && v.Y < bounds.Y +}