Files
advent-of-code-go/2019/day18/part1/main.go
T
2020-09-06 20:12:08 -04:00

287 lines
9.5 KiB
Go

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
}