mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-05-18 19:13:27 +02:00
might be making a mistake starting 2018...
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"adventofcode/util"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
input := util.ReadFile("../input.txt")
|
||||
linesSli := strings.Split(input, "\n")
|
||||
|
||||
grid := make([][]string, len(linesSli)) // string might be excessive, but it will be easier to look at
|
||||
for i, line := range linesSli {
|
||||
grid[i] = strings.Split(line, "")
|
||||
}
|
||||
|
||||
// get all the coordinates of keys. will be used as starting points for dijkstra's searches
|
||||
keyToCoordinates := getCoordinatesOfKeys(grid)
|
||||
|
||||
// initialize Graph
|
||||
graph := MakeGraph()
|
||||
|
||||
// for every key, generate a new dijkstra grid where all distances are set to MAX SAFE INT, and the key's distance is set to 0
|
||||
for key, sourceCoordinates := range keyToCoordinates {
|
||||
// initialize a dijkstra grid for each key
|
||||
dijkstraGrid := MakeDijkstraGrid(grid, sourceCoordinates)
|
||||
|
||||
// step through grid until complete, handleFrontOfQueue returns true when the queue is empty...
|
||||
for !dijkstraGrid.handleFrontOfQueue() {
|
||||
}
|
||||
// small space optimization, overwrite the dijkstra queue b/c the underlying array is alive
|
||||
dijkstraGrid.queue = nil
|
||||
|
||||
// update graph for all edges from `key` to all other keys
|
||||
for destinationKey, destinationCoordinates := range keyToCoordinates {
|
||||
if key != destinationKey && destinationKey != "@" {
|
||||
row, col := destinationCoordinates[0], destinationCoordinates[1]
|
||||
// pass the key being pathed FROM and the dijkstraNode (found on the grid) of the key found
|
||||
graph.AddEdges(key, dijkstraGrid.grid[row][col])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dfsStartTimestamp := time.Now()
|
||||
// first off (recursive, memoized) dfs method on graph that finds minimum distance
|
||||
fmt.Printf("DFS result: %v\n\nFinished in %v\n\n", graph.dfsMinmumDistance(), time.Since(dfsStartTimestamp))
|
||||
}
|
||||
|
||||
// get a map of the coordinates of points of interests
|
||||
func getCoordinatesOfKeys(grid [][]string) map[string][2]int {
|
||||
pointsOfInterest := make(map[string][2]int)
|
||||
for row, rowSli := range grid {
|
||||
for col, cell := range rowSli {
|
||||
switch {
|
||||
case cell == "@":
|
||||
pointsOfInterest["@"] = [2]int{row, col}
|
||||
// typecase cell to its ASCII value
|
||||
case int(cell[0]) >= 'a' && int(cell[0]) <= 'z':
|
||||
pointsOfInterest[cell] = [2]int{row, col}
|
||||
}
|
||||
}
|
||||
}
|
||||
return pointsOfInterest
|
||||
}
|
||||
|
||||
/********************************************************************************************
|
||||
DIJKSTRA GRID Code
|
||||
*********************************************************************************************/
|
||||
|
||||
// DijkstraGrid stores the grid itself and also the needed queue to traverse through the grid
|
||||
type DijkstraGrid struct {
|
||||
grid [][]*dijkstraNode
|
||||
queue [][2]int // coordinates to be traversed next
|
||||
}
|
||||
|
||||
// dijkstraNode is each cell within a DijkstraGrid.grid
|
||||
type dijkstraNode struct {
|
||||
value string // string of the floor type (key, door, floor?, wall)
|
||||
distance int // distance from the source
|
||||
keysFound map[string]bool // keys that have been run into
|
||||
keysNeeded map[string]bool // keys needed to get to this node, i.e. all doors passed thorugh
|
||||
seen bool
|
||||
}
|
||||
|
||||
// MakeDijkstraGrid initializes a 2D grid of dijkstraNodes with distances initialized to the max safe integer
|
||||
func MakeDijkstraGrid(grid [][]string, startCoords [2]int) *DijkstraGrid {
|
||||
finalGrid := make([][]*dijkstraNode, len(grid))
|
||||
startKey := grid[startCoords[0]][startCoords[1]]
|
||||
for row, rowSli := range grid {
|
||||
// initialize the row's slice in the finalGrid
|
||||
finalGrid[row] = make([]*dijkstraNode, len(grid[0]))
|
||||
for col, cellString := range rowSli {
|
||||
finalGrid[row][col] = &dijkstraNode{
|
||||
value: cellString,
|
||||
distance: math.MaxInt32, // maximum safe integer, effectively 2^31 - 1
|
||||
keysFound: map[string]bool{startKey: true}, // initialize with the starting key
|
||||
keysNeeded: make(map[string]bool), // empty map for now
|
||||
seen: false, // initialize as false
|
||||
}
|
||||
// remove the "@" key because it's not a key... it's just the starting point
|
||||
delete(finalGrid[row][col].keysFound, "@")
|
||||
}
|
||||
}
|
||||
|
||||
// set properties of starting coordinate
|
||||
// distance = 0
|
||||
finalGrid[startCoords[0]][startCoords[1]].distance = 0
|
||||
finalGrid[startCoords[0]][startCoords[1]].seen = true
|
||||
queue := [][2]int{
|
||||
[2]int{startCoords[0], startCoords[1]},
|
||||
}
|
||||
return &DijkstraGrid{finalGrid, queue}
|
||||
}
|
||||
|
||||
var dRow [4]int = [4]int{0, 0, -1, 1}
|
||||
var dCol [4]int = [4]int{-1, 1, 0, 0}
|
||||
|
||||
// handleFrontOfQueue does just that, returns a true if queue is empty upon completion
|
||||
func (dijk *DijkstraGrid) handleFrontOfQueue() (queueIsEmpty bool) {
|
||||
row, col := dijk.queue[0][0], dijk.queue[0][1]
|
||||
cell := dijk.grid[row][col]
|
||||
|
||||
// mark as seen
|
||||
cell.seen = true
|
||||
|
||||
// if key is found, add to cell details
|
||||
if ascii := int(cell.value[0]) - 'a'; ascii >= 0 && ascii < 26 {
|
||||
cell.keysFound[cell.value] = true
|
||||
}
|
||||
|
||||
// if door is found, add to keysNeeded as a lowercase (easier comparison later)
|
||||
if ascii := int(cell.value[0]) - 'A'; ascii >= 0 && ascii < 26 {
|
||||
cell.keysNeeded[string('a'+ascii)] = true
|
||||
}
|
||||
|
||||
// push neighbors into queue IF not already seen and not walls
|
||||
for i := 0; i < 4; i++ {
|
||||
neighborRow, neighborCol := row+dRow[i], col+dCol[i]
|
||||
neighborCell := dijk.grid[neighborRow][neighborCol]
|
||||
if !neighborCell.seen && neighborCell.value != "#" {
|
||||
// add to queue
|
||||
dijk.queue = append(dijk.queue, [2]int{neighborRow, neighborCol})
|
||||
|
||||
// increment its distance by 1
|
||||
neighborCell.distance = cell.distance + 1
|
||||
|
||||
// NOTE manually copying these, otherwise they'll be pointing to the same underlying map pointer
|
||||
// copy keysFound (still carrying the same keys)
|
||||
for key := range cell.keysFound {
|
||||
neighborCell.keysFound[key] = true
|
||||
}
|
||||
|
||||
// copy keysNeeded as well
|
||||
for key := range cell.keysNeeded {
|
||||
neighborCell.keysNeeded[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dequeue by rescoping the slice
|
||||
dijk.queue = dijk.queue[1:]
|
||||
|
||||
// if queue is empty, there are no more paths to generate, return a true
|
||||
if len(dijk.queue) == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/********************************************************************************************
|
||||
GRAPH Code
|
||||
*********************************************************************************************/
|
||||
|
||||
// Graph stores the edges between keys by storing all paths from a particular key to all other keys
|
||||
// all cells in this 2D MAP will be a dijkstraNode because those contain all the needed data
|
||||
// such as distance from last key, the keysNeeded to take this path
|
||||
type Graph struct {
|
||||
keysToKeys map[string]map[string]*dijkstraNode
|
||||
}
|
||||
|
||||
// MakeGraph initializes a Graph instance and returns a pointer to it
|
||||
func MakeGraph() *Graph {
|
||||
return &Graph{
|
||||
keysToKeys: make(map[string]map[string]*dijkstraNode),
|
||||
}
|
||||
}
|
||||
|
||||
// AddEdges takes in the key being pathed FROM, and the dijkstraNode of a key pathed TO
|
||||
// and adds that edge to the graph
|
||||
func (graph *Graph) AddEdges(startKey string, destinationNode *dijkstraNode) {
|
||||
// initialize this "row" of the map if it does not exist yet
|
||||
if graph.keysToKeys[startKey] == nil {
|
||||
graph.keysToKeys[startKey] = make(map[string]*dijkstraNode)
|
||||
}
|
||||
// add the destination node to the graph
|
||||
keyFound := destinationNode.value
|
||||
graph.keysToKeys[startKey][keyFound] = destinationNode
|
||||
}
|
||||
|
||||
func (graph *Graph) dfsMinmumDistance() int {
|
||||
// recursive function that dives through graph
|
||||
var traverse func(string, map[string]bool) int
|
||||
|
||||
// Implement cache for traverse function
|
||||
// caches the "<entry key>:<ordered keys found>" to resulting distance
|
||||
cache := map[string]int{}
|
||||
|
||||
// NOTE helper function that leverages the above cache
|
||||
// Traverse should return the minimum distance to a completion for these inputs
|
||||
// inputs are: 1. the entry point (the key to generate a path FROM)
|
||||
// 2. the keys that have been found so far
|
||||
traverse = func(entry string, keysFound map[string]bool) int {
|
||||
shortestFromThisNode := math.MaxInt32
|
||||
|
||||
cacheKey := makeCacheKey(entry, keysFound, len(graph.keysToKeys)-1)
|
||||
|
||||
// cache hit
|
||||
if cacheVal, found := cache[cacheKey]; found {
|
||||
return cacheVal
|
||||
}
|
||||
|
||||
// base case: all keys found, no more distance to be traveled, i.e. zero
|
||||
if len(keysFound) == len(graph.keysToKeys)-1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// iterate over all possible destination nodes.
|
||||
nextEdges := graph.keysToKeys[entry]
|
||||
for nextKey, node := range nextEdges {
|
||||
// only traverse if key has not been found yet AND all needed keys have been collected already
|
||||
if !keysFound[nextKey] && haveNeededKeys(keysFound, node.keysNeeded) {
|
||||
// add the nextKey
|
||||
keysFound[nextKey] = true
|
||||
|
||||
// update the shortestFromThisNode if applicable
|
||||
distanceToEnd := node.distance + traverse(nextKey, keysFound)
|
||||
if distanceToEnd < shortestFromThisNode {
|
||||
shortestFromThisNode = distanceToEnd
|
||||
}
|
||||
|
||||
// backtrack - remove the key
|
||||
delete(keysFound, nextKey)
|
||||
}
|
||||
}
|
||||
|
||||
cache[cacheKey] = shortestFromThisNode
|
||||
return shortestFromThisNode
|
||||
}
|
||||
|
||||
// fire off initially
|
||||
return traverse("@", map[string]bool{})
|
||||
}
|
||||
|
||||
// helper function to generate a cache string key
|
||||
// cache string is of the form entryKey:allKeysToFind
|
||||
func makeCacheKey(entry string, keysFound map[string]bool, totalKeys int) string {
|
||||
// TODO: move this into the graph? to avoid making it 910238190382 times
|
||||
allKeys := make([]string, totalKeys)
|
||||
for i := 0; i < totalKeys; i++ {
|
||||
allKeys[i] = string(int('a') + i)
|
||||
}
|
||||
|
||||
cacheKey := entry
|
||||
// generate
|
||||
for i := 0; i < totalKeys; i++ {
|
||||
char := string(int('a') + i)
|
||||
if !keysFound[char] {
|
||||
cacheKey += char
|
||||
}
|
||||
}
|
||||
|
||||
return cacheKey
|
||||
}
|
||||
|
||||
// simple helper function to make sure that all keysNeeded are in keysFound
|
||||
func haveNeededKeys(keysFound, keysNeeded map[string]bool) bool {
|
||||
for key := range keysNeeded {
|
||||
if !keysFound[key] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user