diff --git a/.gitignore b/.gitignore index fe93a5f..47e7700 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ # Other stuff *.log -input.txt \ No newline at end of file +input.txt +.vscode \ No newline at end of file diff --git a/day-14/day-14 b/day-14/day-14 new file mode 100755 index 0000000..eb3baab Binary files /dev/null and b/day-14/day-14 differ diff --git a/day-14/main.go b/day-14/main.go new file mode 100644 index 0000000..50ed81d --- /dev/null +++ b/day-14/main.go @@ -0,0 +1,165 @@ +/* Day 14: Extended Polymerization */ +package main + +import ( + "AoC2021/utils/args" + "AoC2021/utils/log" + "AoC2021/utils/timer" + "bufio" + "math" + "os" + "strconv" + "strings" +) + +func main() { + argMap := args.Parse(os.Args) + setLogLevel(argMap) + selectedPart, partSpecified := argMap["part"] + + log.Println(log.NORMAL, "--- Day 14: Extended Polymerization ---") + timer.Start() + if !partSpecified || selectedPart == "1" { + part1() + timer.TickAtLevel(log.NORMAL) + log.Println(log.NORMAL) + } + + if !partSpecified || selectedPart == "2" { + part2() + timer.TickAtLevel(log.NORMAL) + log.Println(log.NORMAL) + } +} + +func part1() { + log.Println(log.NORMAL, "* Part 1 *") + log.Println(log.NORMAL, " Goal: Apply 10 steps of pair insertion to the polymer template and find the most and least common elements in the result.") + log.Println(log.NORMAL, " Answer: What do you get if you take the quantity of the most common element and subtract the quantity of the least common element?") + + doTheThing(10) +} + +func part2() { + log.Println(log.NORMAL, "* Part 2 *") + log.Println(log.NORMAL, " Goal: Apply 40 steps of pair insertion to the polymer template and find the most and least common elements in the result.") + log.Println(log.NORMAL, " Answer: What do you get if you take the quantity of the most common element and subtract the quantity of the least common element?") + + doTheThing(40) +} + +func doTheThing(totalSteps int) { + + template, pairs := readInput() + log.Println(log.DIAGNOSTIC, template, pairs) + + // perform the loop 'totalSteps' times + for step := 0; step < totalSteps; step++ { + // starting from the front of the template, check two-letter pairs + for tIdx := 0; tIdx < len(template)-1; tIdx++ { + // the lookup key will be the next two letters in the template + key := template[tIdx : tIdx+2] + // if we find a match in the pairs map + value, found := pairs[key] + if found { + // insert the new character between the original two letters + template = template[:tIdx+1] + value + template[tIdx+1:] + // increment the idx counter once more so that we skip over the character that we just inserted + tIdx++ + } + } + log.Printf(log.DEBUG, "After step #%d template is length %d\n", step+1, len(template)) + timer.TickAtLevel(log.DEBUG) + } + + // COUNT all character occurances + countMap := make(map[byte]int) + + for idx := 0; idx < len(template); idx++ { + countMap[template[idx]]++ + } + log.Println(log.DIAGNOSTIC, "CountMap:", countMap) + + mostCommonLetter, mostCommonCount := getMostCommon(countMap) + leastCommonLetter, leastCommonCount := getLeastCommon(countMap) + + log.Printf(log.NORMAL, "After %d steps, the most common (%s=%d) minus the least common (%s=%d) is %d\n", totalSteps, mostCommonLetter, mostCommonCount, leastCommonLetter, leastCommonCount, mostCommonCount-leastCommonCount) +} + +func check(e error) { + if e != nil { + log.Println(log.DEBUG, "Oh snap!") + panic(e) + } +} + +func setLogLevel(argMap map[string]string) { + logLevelStr, found := argMap["log"] + if found { + logLevel, err := strconv.Atoi(logLevelStr) + check(err) + log.SetLogLevel(logLevel) + } +} + +func readInput() (string, map[string]string) { + input, err := os.Open("./input.txt") + check(err) + defer input.Close() + + scanner := bufio.NewScanner(input) + lines := 0 + + // TEMPLATE LINE + if !scanner.Scan() { + panic("Error parsing input template") + } + template := scanner.Text() + lines++ + + // NEW LINE + if !scanner.Scan() { + panic("Error parsing input newline") + } + _ = scanner.Text() + lines++ + + // PAIRS + pairs := make(map[string]string) + for scanner.Scan() { + pair := strings.Split(scanner.Text(), " -> ") + if len(pair) != 2 { + panic("Error parsing input pairs") + } + pairs[pair[0]] = pair[1] + lines++ + } + + log.Printf(log.NORMAL, "Read %d pairs from %d lines in input\n", len(pairs), lines) + + return template, pairs +} + +func getMostCommon(countMap map[byte]int) (string, int) { + mostCommonStr := "" + mostCommonCount := -math.MaxInt + for key, value := range countMap { + if value > mostCommonCount { + mostCommonStr, mostCommonCount = string(key), value + } + } + + return mostCommonStr, mostCommonCount +} + +func getLeastCommon(countMap map[byte]int) (string, int) { + leastCommonStr := "" + leastCommonCount := math.MaxInt + for key, value := range countMap { + if value < leastCommonCount { + leastCommonStr, leastCommonCount = string(key), value + } + } + + return leastCommonStr, leastCommonCount +} diff --git a/day-14/main_test.go b/day-14/main_test.go new file mode 100644 index 0000000..e91d8d6 --- /dev/null +++ b/day-14/main_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "AoC2021/utils/assert" + "testing" +) + +func TestDay14(t *testing.T) { + assert.IsTrue(t, true, "cool") + assert.EqualsStr(t, "foo", "bar", "baz") +} diff --git a/day-15/main.go b/day-15/main.go new file mode 100644 index 0000000..fabde8e --- /dev/null +++ b/day-15/main.go @@ -0,0 +1,307 @@ +/* Day 15: Chiton */ +package main + +import ( + "AoC2021/utils/arrays" + "AoC2021/utils/color" + "AoC2021/utils/log" + "AoC2021/utils/numbers" + "AoC2021/utils/timer" + "bufio" + "math" + "os" + "strconv" + "strings" +) + +func check(e error) { + if e != nil { + log.Println(log.DEBUG, "Oh snap!") + panic(e) + } +} + +func main() { + log.Println(log.NORMAL, "--- Day 15: Chiton ---") + timer.Start() + part1() + timer.Tick() + log.Println(log.NORMAL) + + // TODO: Write a faster algorithm + // part2() + // timer.Tick() + // log.Println(log.NORMAL) +} + +func part1() { + log.Println(log.NORMAL, "* Part 1 *") + log.Printf(log.NORMAL, color.Green) + log.Println(log.NORMAL, " Goal: Your goal is to find a path with the lowest total risk.") + log.Println(log.NORMAL, " Answer: What is the lowest total risk of any path from the top left to the bottom right?") + log.Printf(log.NORMAL, color.Reset) + + risks := readRiskNodes() + costs := initCosts(risks) + + calcCosts(risks, &costs) + lastRow := len(costs) - 1 + lastCol := len(costs[lastRow]) - 1 + exitCost := costs[lastRow][lastCol] - costs[0][0] + + log.Printf(log.NORMAL, "The lowest risk cost from row=0 col=0 to row=%d col=%d is %d\n", lastRow, lastCol, exitCost) + walkPath(risks, costs) +} + +func part2() { + log.SetLogLevel(log.DEBUG) + + log.Println(log.NORMAL, "* Part 2 *") + log.Printf(log.NORMAL, color.Green) + log.Println(log.NORMAL, " Goal: Calc using the full map") + log.Println(log.NORMAL, " Answer: What is the lowest total risk of any path from the top left to the bottom right?") + log.Printf(log.NORMAL, color.Reset) + + risks := readRiskNodes() + expandRisks(&risks) + costs := initCosts(risks) + + calcCosts(risks, &costs) + lastRow := len(costs) - 1 + lastCol := len(costs[lastRow]) - 1 + exitCost := costs[lastRow][lastCol] - costs[0][0] + + log.Printf(log.NORMAL, "The lowest risk cost from row=0 col=0 to row=%d col=%d is %d\n", lastRow, lastCol, exitCost) + walkPath(risks, costs) +} + +type RiskNodes [][]int + +func readRiskNodes() RiskNodes { + input, err := os.Open("./input.txt") + check(err) + defer input.Close() + + scanner := bufio.NewScanner(input) + lines := 0 + + var risks RiskNodes + + for scanner.Scan() { + line := scanner.Text() + lines++ + // We'll just hope that our input is well formed + risks = append(risks, arrays.MapStrToInt(strings.Split(line, ""), func(s string) int { i, _ := strconv.Atoi(s); return i })) + } + + log.Printf(log.DEBUG, "Read %d lines from input\n", lines) + return risks +} + +func expandRisks(risks *RiskNodes) { + multiplier := 5 + // expand out all the lines to the new width + rowLen, colLen := len(*risks), len((*risks)[0]) + + // extend each row N times + for rIdx, row := range *risks { + for m := 1; m < multiplier; m++ { + (*risks)[rIdx] = append((*risks)[rIdx], row...) + } + } + // copy each row N times + for m := 1; m < multiplier; m++ { + for rIdx := 0; rIdx < rowLen; rIdx++ { + (*risks) = append((*risks), (*risks)[0]) + } + } + // increment the risk number in each cell + for rIdx, row := range *risks { + for cIdx, value := range row { + if rIdx < rowLen && cIdx < colLen { + continue + } + // add 1 for every section right or down that we've copied + newVal := (value + rIdx/rowLen + cIdx/colLen) % 10 + (*risks)[rIdx][cIdx] = newVal + } + } + log.Println(log.DIAGNOSTIC, "Expanded risk nodes to new size") +} + +type CostNodes [][]int + +func initCosts(risks RiskNodes) CostNodes { + var costs CostNodes + for _, row := range risks { + costs = append(costs, make([]int, len(row))) + } + + log.Printf(log.DIAGNOSTIC, "Initialized costs node of size row=%d by col=%d\n", len(risks), len(risks[0])) + + return costs +} + +var filledNodes int +var recalcCount int +var totalNodes int + +func calcCosts(risks RiskNodes, costs *CostNodes) { + // the goal is to MINIMIZE the risk cost from traveling from START to END + // starting from the current node check each adjacent valid node + // if the total cost on that node is 0 or if the toal cost is > the current node total cost + next node travel cost + // then update that next node's total and continue iterating at it + // otherwise just stop + (*costs)[0][0] = math.MaxInt // so that we don't go back to start + + filledNodes = 1 + totalNodes = len(*costs) * len((*costs)[0]) + log.Println(log.DIAGNOSTIC, "Starting calc from (0,0)") + calcAllNeighbors(risks, costs, 0, 0, 0) +} + +type RowCol struct { + r int + c int +} + +func calcAllNeighbors(risks RiskNodes, costs *CostNodes, row, col, currentTotal int) { + if !nodeIsValid(*costs, row, col) { + return + } + log.Printf(log.DIAGNOSTIC, "Calculating neighbors at row=%d col=%d\n", row, col) + if (*costs)[row][col] == 0 || (*costs)[row][col] > currentTotal+risks[row][col] { + if (*costs)[row][col] == 0 { + filledNodes++ + if filledNodes%1000 == 0 { + log.Printf(log.DEBUG, "Calculated %d / %d nodes\n", filledNodes, totalNodes) + } + } else { + recalcCount++ + if recalcCount%10000 == 0 { + log.Printf(log.DEBUG, "Recalculated %d / ?? nodes\n", recalcCount) + } + } + (*costs)[row][col] = currentTotal + risks[row][col] + /* UP */ calcAllNeighbors(risks, costs, row-1, col, currentTotal+risks[row][col]) + /* DOWN */ calcAllNeighbors(risks, costs, row+1, col, currentTotal+risks[row][col]) + /* LEFT */ calcAllNeighbors(risks, costs, row, col-1, currentTotal+risks[row][col]) + /* RIGHT */ calcAllNeighbors(risks, costs, row, col+1, currentTotal+risks[row][col]) + } else { + log.Printf(log.DIAGNOSTIC, "Hit a calc dead end at row=%d, col=%d\n", row, col) + } +} + +func nodeIsValid(costs CostNodes, row, col int) bool { + switch { + case col < 0, col >= len(costs): + return false + case row < 0, row >= len(costs[col]): + return false + default: + return true + } +} + +func walkPath(risks RiskNodes, costs CostNodes) { + // start from the end and go toward the beginning then flip the script + row := len(costs) - 1 + col := len(costs[row]) - 1 + + log.Printf(log.DIAGNOSTIC, "Starting to walk path at row=%d, col=%d\n", row, col) + + nodes := []RowCol{{row, col}} + + sum := 0 + for ; row != 0 || col != 0; row, col = lowestNeighbor(costs, row, col) { + nodes = append(nodes, RowCol{row, col}) + sum += risks[row][col] + } + nodes = append(nodes, RowCol{row, col}) + // don't count 0,0 in the sum + + // print all the nodes and if they are on the path then color them + printColor := color.DarkGray + + for row := 0; row < len(risks); row++ { + for col := 0; col < len(risks[row]); col++ { + // for the first and last node, print in yellow + // for nodes on the path, print in green + // else print in normal + if (row == 0 && col == 0) || (row == len(risks)-1 && col == len(risks[row])-1) { + printColor = color.YellowBold + log.Printf(log.NORMAL, printColor) + } else if inPath(nodes, row, col) { + if printColor != color.BlueBold { + printColor = color.BlueBold + log.Printf(log.NORMAL, printColor) + } + } else if printColor != color.DarkGray { + printColor = color.DarkGray + log.Printf(log.NORMAL, printColor) + } + + log.Printf(log.NORMAL, "%d", risks[row][col]) + } + log.Println(log.NORMAL) + } + log.Printf(log.NORMAL, color.Reset) + + // iterate through backward to go from start node to end + row, col = nodes[len(nodes)-1].r, nodes[len(nodes)-1].c + log.Printf(log.NORMAL, color.Blue+"%d (%03d)"+color.Reset, risks[row][col], costs[row][col]) + for nIdx := len(nodes) - 2; nIdx >= 0; nIdx-- { + row, col = nodes[nIdx].r, nodes[nIdx].c + log.Printf(log.NORMAL, " -> "+color.Blue+"%d (%03d)"+color.Reset, risks[row][col], costs[row][col]) + if (len(nodes)-nIdx)%16 == 0 { + log.Println(log.NORMAL) + } + } + log.Println(log.NORMAL) + + log.Printf(log.DEBUG, "Total risk cost is %d in %d steps\n", sum, len(nodes)) +} + +func lowestNeighbor(costs CostNodes, row, col int) (int, int) { + // if we're at 0,1 or 1,0 then just go to 0,0 + if (row == 0 && col == 1) || (row == 1 && col == 0) { + return 0, 0 + } + + // compare UP, DOWN, LEFT, RIGHT + upCost, downCost, leftCost, rightCost := math.MaxInt, math.MaxInt, math.MaxInt, math.MaxInt + if nodeIsValid(costs, row-1, col) { + upCost = costs[row-1][col] + } + if nodeIsValid(costs, row+1, col) { + downCost = costs[row+1][col] + } + if nodeIsValid(costs, row, col-1) { + leftCost = costs[row][col-1] + } + if nodeIsValid(costs, row, col+1) { + rightCost = costs[row][col+1] + } + + switch numbers.Min(upCost, downCost, leftCost, rightCost) { + case upCost: + return row - 1, col + case downCost: + return row + 1, col + case leftCost: + return row, col - 1 + case rightCost: + return row, col + 1 + default: + panic("at the disco!") + } +} + +func inPath(nodes []RowCol, row, col int) bool { + for _, n := range nodes { + if n.r == row && n.c == col { + return true + } + } + return false +} diff --git a/day-15/main_test.go b/day-15/main_test.go new file mode 100644 index 0000000..361e45e --- /dev/null +++ b/day-15/main_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "AoC2021/utils/assert" + "testing" +) + +func TestDay15(t *testing.T) { + assert.IsTrue(t, true, "cool") + assert.EqualsStr(t, "foo", "bar", "baz") +} diff --git a/makefile b/makefile index 50f7f74..8403698 100644 --- a/makefile +++ b/makefile @@ -8,7 +8,7 @@ init: touch day-$(day)/input.txt; build: - cd day-$(day); go run . + cd day-$(day); go run . log=$(log) part=$(part) test: cd day-$(day); go test diff --git a/utils/args/args.go b/utils/args/args.go new file mode 100644 index 0000000..c013f20 --- /dev/null +++ b/utils/args/args.go @@ -0,0 +1,15 @@ +package args + +import "strings" + +func Parse(args []string) map[string]string { + argMap := make(map[string]string) + for _, arg := range args { + pair := strings.Split(arg, "=") + if len(pair) == 2 { + argMap[pair[0]] = pair[1] + } + } + + return argMap +} diff --git a/utils/color/color.go b/utils/color/color.go new file mode 100644 index 0000000..29f0fd6 --- /dev/null +++ b/utils/color/color.go @@ -0,0 +1,131 @@ +package color + +import "runtime" + +var Reset = "\033[0m" +var Red = "\033[31m" +var Green = "\033[32m" +var Yellow = "\033[33m" +var Blue = "\033[34m" +var Purple = "\033[35m" +var Cyan = "\033[36m" +var LightGray = "\033[37m" +var DarkGray = "\033[30m" +var White = "\033[97m" + +var BlackBold = "\033[1;30m" +var RedBold = "\033[1;31m" +var GreenBold = "\033[1;32m" +var YellowBold = "\033[1;33m" +var BlueBold = "\033[1;34m" +var PurpleBold = "\033[1;35m" +var CyanBold = "\033[1;36m" +var WhiteBold = "\033[1;37m" + +var BlackUnderline = "\033[4;30m" +var RedUnderline = "\033[4;31m" +var GreenUnderline = "\033[4;32m" +var YellowUnderline = "\033[4;33m" +var BlueUnderline = "\033[4;34m" +var PurpleUnderline = "\033[4;35m" +var CyanUnderline = "\033[4;36m" +var WhiteUnderline = "\033[4;37m" + +var BlackBackground = "\033[40m" +var RedBackground = "\033[41m" +var GreenBackground = "\033[42m" +var YellowBackground = "\033[43m" +var BlueBackground = "\033[44m" +var PurpleBackground = "\033[45m" +var CyanBackground = "\033[46m" +var WhiteBackground = "\033[47m" + +var BlackHigh = "\033[0;90m" +var RedHigh = "\033[0;91m" +var GreenHigh = "\033[0;92m" +var YellowHigh = "\033[0;93m" +var BlueHigh = "\033[0;94m" +var PurpleHigh = "\033[0;95m" +var CyanHigh = "\033[0;96m" +var WhiteHigh = "\033[0;97m" + +var BlackBoldHigh = "\033[1;90m" +var RedBoldHigh = "\033[1;91m" +var GreenBoldHigh = "\033[1;92m" +var YellowBoldHigh = "\033[1;93m" +var BlueBoldHigh = "\033[1;94m" +var PurpleBoldHigh = "\033[1;95m" +var CyanBoldHigh = "\033[1;96m" +var WhiteBoldHigh = "\033[1;97m" + +var BlackHighBack = "\033[0;100m" +var RedHighBack = "\033[0;101m" +var GreenHighBack = "\033[0;102m" +var YellowHighBack = "\033[0;103m" +var BlueHighBack = "\033[0;104m" +var PurpleHighBack = "\033[0;105m" +var CyanHighBack = "\033[0;106m" +var WhiteHighBack = "\033[0;107m" + +func init() { + if runtime.GOOS == "windows" { + Reset = "" + Red = "" + Green = "" + Yellow = "" + Blue = "" + Purple = "" + Cyan = "" + LightGray = "" + DarkGray = "" + White = "" + BlackBold = "" + RedBold = "" + GreenBold = "" + YellowBold = "" + BlueBold = "" + PurpleBold = "" + CyanBold = "" + WhiteBold = "" + BlackUnderline = "" + RedUnderline = "" + GreenUnderline = "" + YellowUnderline = "" + BlueUnderline = "" + PurpleUnderline = "" + CyanUnderline = "" + WhiteUnderline = "" + BlackBackground = "" + RedBackground = "" + GreenBackground = "" + YellowBackground = "" + BlueBackground = "" + PurpleBackground = "" + CyanBackground = "" + WhiteBackground = "" + BlackHigh = "" + RedHigh = "" + GreenHigh = "" + YellowHigh = "" + BlueHigh = "" + PurpleHigh = "" + CyanHigh = "" + WhiteHigh = "" + BlackBoldHigh = "" + RedBoldHigh = "" + GreenBoldHigh = "" + YellowBoldHigh = "" + BlueBoldHigh = "" + PurpleBoldHigh = "" + CyanBoldHigh = "" + WhiteBoldHigh = "" + BlackHighBack = "" + RedHighBack = "" + GreenHighBack = "" + YellowHighBack = "" + BlueHighBack = "" + PurpleHighBack = "" + CyanHighBack = "" + WhiteHighBack = "" + } +} diff --git a/utils/log/log.go b/utils/log/log.go index d36f2b4..4a55ce8 100644 --- a/utils/log/log.go +++ b/utils/log/log.go @@ -9,7 +9,20 @@ const ( DIAGNOSTIC = 3 ) +const ( + CONSOLE = 1 + FILE = 2 +) + var LOG_LEVEL = NORMAL +var LOG_TIMESTAMP = false +var LOG_OUTPUT = CONSOLE + +func SetLogLevel(level int) { + if level >= NONE && level <= DIAGNOSTIC { + LOG_LEVEL = level + } +} func Print(minLevel int, a ...interface{}) (int, error) { if LOG_LEVEL >= minLevel { diff --git a/utils/numbers/numbers.go b/utils/numbers/numbers.go new file mode 100644 index 0000000..c7f9a12 --- /dev/null +++ b/utils/numbers/numbers.go @@ -0,0 +1,11 @@ +package numbers + +func Min(i1 int, a ...int) int { + min := i1 + for _, i := range a { + if i < min { + min = i + } + } + return min +} diff --git a/utils/timer/timer.go b/utils/timer/timer.go index bbba959..5ec67c6 100644 --- a/utils/timer/timer.go +++ b/utils/timer/timer.go @@ -1,6 +1,8 @@ package timer import ( + "AoC2021/utils/color" + "AoC2021/utils/log" "fmt" "time" ) @@ -13,6 +15,14 @@ func Start() { func Tick() { now := time.Now() - fmt.Printf("[Time taken: %s]\n", now.Sub(lastTime).String()) + fmt.Printf(color.Yellow+"[Time taken: %s]\n"+color.Reset, now.Sub(lastTime).String()) lastTime = now } + +func TickAtLevel(logLevel int) { + if log.LOG_LEVEL >= logLevel { + now := time.Now() + log.Printf(logLevel, color.Yellow+"[Time taken: %s]\n"+color.Reset, now.Sub(lastTime).String()) + lastTime = now + } +}