day18 part2 solution - not perfect but it passes :/

This commit is contained in:
Alex Chao
2020-08-06 19:51:52 -04:00
parent 59a0095488
commit d94081fbd9
3 changed files with 392 additions and 14 deletions
+1 -1
View File
@@ -24,4 +24,4 @@ Day | Name | Type of Algo & Notes
15 | Oxygen System | YAY INTCODE 🙄 <br> - Combination of __2D grid searching algo__, __backtracking algo__ and the the Intcode... <br> - I've realized that I really need to stop using x and y for 2D grids and start using row and col because mathematically x is horizontal and y is vertical... My brain is all jumbled up <br> - Created a Robot struct/class that has a computer inside of it. It goes and searches around, collecting data on the floor types at various coordinates. That data is transformed into a 2D grid/array, and then finally fed into a backtracking, searching algorithm to determine the shortest path (turns out there's only one path to the O2 tank...) <br> - part2 is fairly straight forward 2D grid traversing and tagging a spread of oxygen to valid tiles/hallway spaces
16 | Flawed Frequency Transmission | - Some really weird, contrived (as if this whole thing isn't) phase calculator?.. <br> - part2 broke my brain. Optimally calculate subsets by __precalculating running sums__ (linear), then getting subsets by subtracting two of the precalculated running sums. i.e. sub[2:4] = sub[0:4] - sub[0:2] <br> - And also switching pattern recognition to make calculating a particular output O(nlogn)...
17 | Set and Forget | More Intcode... <br> Robot walking around a scaffolding... Fairly similar to previous algos, and a 2D grid traversal again <br> - I feel like I cheated part2 by finding the pattern by eye after printing what the 2D grid looks like. Then it was a matter of giving the intcode computer the corresponding inputs (in a weird format). <br> - Good example of __iterative approaches being better than recursive approaches__ because the recursive approach in "continuous video mode" causes a stack overflow very quickly
18 | Many-Worlds Interpretation | - Oof, this is the hardest algo so far I think... Combination of __Best Pathfinding Algo__ (going to use a BFS/Dijkstra's) and __Graph Traversal__ <br> - I toyed with the idea of using some kind of bitmap to track keys that were found, but ditched it so I wouldn't have to write my own wrapper around bitwise logic... Time complexity comparisons are roughly similar for comparing two maps as comparing two sets of bits, the space complexity is probably much better for 27-bits (@ and a-z) compared to a hashmap, but OH WELL. <br>
18 | Many-Worlds Interpretation | - Oof, this is the hardest algo so far I think... Combination of __Best Pathfinding Algo__ (going to use a BFS/Dijkstra's) and a weighted __Graph Traversal__ with "locked" edges <br> - I toyed with the idea of using some kind of bitmap to track keys that were found, but ditched it so I wouldn't have to write my own wrapper around bitwise logic... Time complexity comparisons are roughly similar for comparing two maps as comparing two sets of bits, the space complexity is probably much better for 27-bits (@ and a-z) compared to a hashmap, but OH WELL. <br> - The graph traversal required memoizing in order to run in any reasonable amount of time. <br> - My part 2 assumptions would not work for every example which is unfortunate :/ I assumed that within a particular quadrant, if the quadrant does not have the key for a door, just remove the door.
+6 -13
View File
@@ -9,13 +9,6 @@ import (
func main() {
input := util.ReadFile("../input.txt")
// ! Tests that overwrite the actual input
// ! Expect 8
// input = "#########\n#b.A.@.a#\n#########"
// ! test: expect 86
// input = "########################\n#f.D.E.e.C.b.A.@.a.B.c.#\n######################.#\n#d.....................#\n########################"
linesSli := strings.Split(input, "\n")
grid := make([][]string, len(linesSli)) // string might be excessive, but it will be easier to look at
@@ -30,9 +23,9 @@ func main() {
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, coordinates := range keyToCoordinates {
for key, sourceCoordinates := range keyToCoordinates {
// initialize a dijkstra grid for each key
dijkstraGrid := MakeDijkstraGrid(grid, coordinates)
dijkstraGrid := MakeDijkstraGrid(grid, sourceCoordinates)
// step through grid until complete, handleFrontOfQueue returns true when the queue is empty...
for !dijkstraGrid.handleFrontOfQueue() {
@@ -41,9 +34,9 @@ func main() {
dijkstraGrid.queue = nil
// update graph for all edges from `key` to all other keys
for otherKey, coordinates := range keyToCoordinates {
if key != otherKey && otherKey != "@" {
row, col := coordinates[0], coordinates[1]
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])
}
@@ -269,7 +262,7 @@ func makeCacheKey(entry string, keysFound map[string]bool, totalKeys int) string
allKeys[i] = string(int('a') + i)
}
cacheKey := entry + ":"
cacheKey := entry
// generate
for i := 0; i < totalKeys; i++ {
char := string(int('a') + i)
+385
View File
@@ -0,0 +1,385 @@
package main
import (
"adventofcode/util"
"fmt"
"sort"
"strings"
)
func main() {
input := util.ReadFile("../input.txt")
linesSli := strings.Split(input, "\n")
// generate four quadrants
quad1, quad2, quad3, quad4 := generateQuadrants(linesSli)
// handle an individual grid, sum answers together to print
one := handleGrid(quad1)
two := handleGrid(quad2)
three := handleGrid(quad3)
four := handleGrid(quad4)
fmt.Printf("Sum for four robots is %v\n", one+two+three+four)
}
// Helper function to do all the things to a grid...
func handleGrid(grid [][]string) int {
removeDoorsWithoutKeys(grid)
keysToCoordinates := getCoordinatesOfKeys(grid)
graph := MakeGraph()
for key, sourceCoordinates := range keysToCoordinates {
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 otherKey, destinationCoordinates := range keysToCoordinates {
if key != otherKey && otherKey != "@" {
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])
}
}
}
// then concurrently write back to the result chan
return graph.dfsMinmumDistance()
}
// 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: 1<<31 - 1, // 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
allKeysNeeded map[string]bool
}
// MakeGraph initializes a Graph instance and returns a pointer to it
func MakeGraph() *Graph {
return &Graph{
keysToKeys: make(map[string]map[string]*dijkstraNode),
allKeysNeeded: make(map[string]bool),
}
}
// 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) {
// add startKey to the allKeysNeeded hashmap
graph.allKeysNeeded[startKey] = true
// 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 := 1<<31 - 1
cacheKey := makeCacheKey(entry, keysFound, graph.allKeysNeeded)
// 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, allKeysNeeded map[string]bool) string {
cacheKey := entry
// generate sorted list of the keys that have NOT been found yet
needToFind := []string{}
for key := range allKeysNeeded {
if !keysFound[key] {
needToFind = append(needToFind, key)
}
}
sort.Strings(needToFind)
return cacheKey + strings.Join(needToFind, "")
}
// 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
}
/****************************************************************************************
INPUT SANITIZATION FUNCTIONS
*****************************************************************************************/
/* Quadrant numbers :) mathematical because why not
II | I
|
--------+---------
|
III | IV
| */
// This function is gross but it does what it says it does... returns four quadrants
func generateQuadrants(linesSli []string) ([][]string, [][]string, [][]string, [][]string) {
quad1 := make([][]string, len(linesSli))
quad2 := make([][]string, len(linesSli))
quad3 := make([][]string, len(linesSli))
quad4 := make([][]string, len(linesSli))
for i, line := range linesSli {
quad1[i] = strings.Split(line, "")
quad2[i] = strings.Split(line, "")
quad3[i] = strings.Split(line, "")
quad4[i] = strings.Split(line, "")
}
// really only need the "@" coordinates, but might as well use this function that I alreade have..
keyToCoordinates := getCoordinatesOfKeys(quad1)
originRow, originCol := keyToCoordinates["@"][0], keyToCoordinates["@"][1]
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
row := originRow - 1 + i
col := originCol - 1 + j
if (row+col)%2 == 0 {
quad1[row][col] = "@"
quad2[row][col] = "@"
quad3[row][col] = "@"
quad4[row][col] = "@"
} else {
quad1[row][col] = "#"
quad2[row][col] = "#"
quad3[row][col] = "#"
quad4[row][col] = "#"
}
}
}
// replace origin with wall
quad1[originRow][originCol] = "#"
quad2[originRow][originCol] = "#"
quad3[originRow][originCol] = "#"
quad4[originRow][originCol] = "#"
// rescope quadrants to point to correct scope of underlying arrays
quad1 = quad1[:originRow+1]
quad2 = quad2[:originRow+1]
for i := range quad1 {
quad1[i] = quad1[i][originCol:]
quad2[i] = quad2[i][:originCol+1]
}
quad3 = quad3[originRow:]
quad4 = quad4[originRow:]
for i := range quad3 {
quad3[i] = quad3[i][:originCol+1]
quad4[i] = quad4[i][originCol:]
}
// return the four quads
return quad1, quad2, quad3, quad4
}
// NOTE this is a BIG assumption that will not work on all inputs, in fact it fails on a lot of the
// note examples... :(
// removes doors from a quadrant if the associated key is not in the quadrant
func removeDoorsWithoutKeys(quadrant [][]string) {
// put all the keys in this quadrant in a hashmap (along with walls, floors and the origin "@")
valuesToKeep := map[string]bool{"#": true, ".": true, "@": true}
for _, rowSli := range quadrant {
for _, cell := range rowSli {
if ascii := int(cell[0] - 'a'); ascii >= 0 && ascii < 26 {
valuesToKeep[string(ascii+'a')] = true
}
}
}
// iterate through the quadrant again, this time removing any capital letters
// who's lower case representation is NOT in the valuesToKeep hashmap
for row, rowSli := range quadrant {
for col, cell := range rowSli {
if lower := strings.ToLower(cell); !valuesToKeep[lower] {
quadrant[row][col] = "." // replace with an empty hallway
}
}
}
}