From 41c5e86e7cc67a8bd51b902e7a638989a12b89d5 Mon Sep 17 00:00:00 2001 From: Russell Champoux Date: Fri, 10 Dec 2021 11:22:03 -0500 Subject: [PATCH] day 9 and 10 --- day-10/main.go | 215 +++++++++++++++++++++++++++++++++++++ day-10/main_test.go | 11 ++ day-9/main.go | 233 +++++++++++++++++++++++++++++++++++++++++ day-9/main_test.go | 11 ++ main_test.go.template | 2 +- utils/arrays/arrays.go | 8 ++ 6 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 day-10/main.go create mode 100644 day-10/main_test.go create mode 100644 day-9/main.go create mode 100644 day-9/main_test.go diff --git a/day-10/main.go b/day-10/main.go new file mode 100644 index 0000000..0ce24e9 --- /dev/null +++ b/day-10/main.go @@ -0,0 +1,215 @@ +/* Day 10: Syntax Scoring */ +package main + +import ( + "AoC2021/utils/arrays" + "AoC2021/utils/log" + "AoC2021/utils/timer" + "bufio" + "os" + "sort" + "strings" +) + +func check(e error) { + if e != nil { + log.Println(log.DEBUG, "Oh snap!") + panic(e) + } +} + +func main() { + log.Println(log.NORMAL, "--- Day 10: Syntax Scoring ---") + timer.Start() + part1() + timer.Tick() + log.Println(log.NORMAL) + + part2() + timer.Tick() + log.Println(log.NORMAL) +} + +func part1() { + log.LOG_LEVEL = log.DEBUG + log.Println(log.NORMAL, "* Part 1 *") + log.Println(log.NORMAL, " Goal: Find the first illegal character in each corrupted line of the navigation subsystem.") + log.Println(log.NORMAL, " Answer: What is the total syntax error score for those errors?") + + input, err := os.Open("./input.txt") + check(err) + defer input.Close() + + scanner := bufio.NewScanner(input) + lines := 0 + tokenCount := 0 + totalScore := 0 + + for scanner.Scan() { + line := scanner.Text() + _, errScore := validateNavLine(line) + totalScore += errScore + lines++ + tokenCount += len(line) + } + + log.Printf(log.NORMAL, "Read %d lines with %d tokens from input\n", lines, tokenCount) + log.Printf(log.NORMAL, "The total syntax error score is %d\n", totalScore) +} + +func part2() { + log.Println(log.NORMAL, "* Part 2 *") + log.Println(log.NORMAL, " Goal: Find the completion string for each incomplete line, score the completion strings, and sort the scores.") + log.Println(log.NORMAL, " Answer: What is the middle score?") + + input, err := os.Open("./input.txt") + check(err) + defer input.Close() + + scanner := bufio.NewScanner(input) + lines := 0 + tokenCount := 0 + + var autoScores []int + + for scanner.Scan() { + line := scanner.Text() + + lines++ + tokenCount += len(line) + + remainingStr, errScore := validateNavLine(line) + if errScore > 0 || len(remainingStr) == 0 { + continue + } + + autoScores = append(autoScores, getAutoScore(remainingStr)) + } + + // stort the scores + sort.Slice(autoScores, func(i, j int) bool { return autoScores[i] < autoScores[j] }) + // find the middle-ist index + middleIdx := len(autoScores) / 2 + + log.Printf(log.NORMAL, "Read %d lines with %d tokens from input\n", lines, tokenCount) + log.Printf(log.NORMAL, "Autocompleted %d lines\n", len(autoScores)) + log.Printf(log.NORMAL, "The middle-ist auto-correct score at pos %d is %d\n", middleIdx, autoScores[middleIdx]) +} + +// validates the nav line and returns a non-zero value as a syntax error score +func BAD_validateNavLine(line string) int { + // split the line into tokens + tokens := strings.Split(line, "") + invalidStr := "An invalid token '%s' was found at position %3d while validating string: %s\n" + + // I thought of doing this generically but there are only 4 hard-coded options so just do it manually + parens, brackets, braces, gators := 0, 0, 0, 0 + + for idx, token := range tokens { + switch token { + case "(": + parens++ + case "[": + brackets++ + case "{": + braces++ + case "<": + gators++ + case ")": + if parens > 0 { + parens-- + } else { + log.Printf(log.DEBUG, invalidStr, token, idx, line) + return 3 + } + case "]": + if brackets > 0 { + brackets-- + } else { + log.Printf(log.DEBUG, invalidStr, token, idx, line[:idx+1]) + return 57 + } + case "}": + if braces > 0 { + braces-- + } else { + log.Printf(log.DEBUG, invalidStr, token, idx, line) + return 1197 + } + case ">": + if gators > 0 { + gators-- + } else { + log.Printf(log.DEBUG, invalidStr, token, idx, line) + return 25137 + } + } + } + + return 0 +} + +func validateNavLine(line string) (string, int) { + reducedLine := reduceNavLine(line) + if len(reducedLine) == 0 { + return reducedLine, 0 + } + // find the first closing token + firstCloseTokenIdx := strings.IndexAny(reducedLine, ")]}>") + // if no closing tokens are found, this is just an incomplete line (not corrupt) + if firstCloseTokenIdx == -1 { + return reducedLine, 0 + } + log.Printf(log.DIAGNOSTIC, "First invalid token is at %d on %s\n", firstCloseTokenIdx, reducedLine) + switch reducedLine[firstCloseTokenIdx] { + case ')': + return reducedLine, 3 + case ']': + return reducedLine, 57 + case '}': + return reducedLine, 1197 + case '>': + return reducedLine, 25137 + } + + return reducedLine, 0 +} + +func reduceNavLine(line string) string { + //recursively remove empty chunks from the line + oldLen := len(line) + for { + line = strings.ReplaceAll(line, "()", "") + line = strings.ReplaceAll(line, "[]", "") + line = strings.ReplaceAll(line, "{}", "") + line = strings.ReplaceAll(line, "<>", "") + + if len(line) == oldLen { + break + } + oldLen = len(line) + } + + return line +} + +// get an auto-complete score from a line that has already been reduced +func getAutoScore(reducedLine string) int { + // we'll have to auto-complete tokens in REVERSE order of the opening tokens in the reducedLine + score := 0 + for _, token := range arrays.Reverse(strings.Split(reducedLine, "")) { + score *= 5 + switch token { + case "(": + score += 1 + case "[": + score += 2 + case "{": + score += 3 + case "<": + score += 4 + } + } + + return score +} diff --git a/day-10/main_test.go b/day-10/main_test.go new file mode 100644 index 0000000..04a0a19 --- /dev/null +++ b/day-10/main_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "AoC2021/utils/assert" + "testing" +) + +func TestDay10(t *testing.T) { + assert.IsTrue(t, true, "cool") + assert.EqualsStr(t, "foo", "bar", "baz") +} diff --git a/day-9/main.go b/day-9/main.go new file mode 100644 index 0000000..99e5be1 --- /dev/null +++ b/day-9/main.go @@ -0,0 +1,233 @@ +/* Day 9: Smoke Basin */ +package main + +import ( + "AoC2021/utils/arrays" + "AoC2021/utils/log" + "AoC2021/utils/timer" + "bufio" + "os" + "sort" + "strconv" + "strings" +) + +func check(e error) { + if e != nil { + log.Println(log.DEBUG, "Oh snap!") + panic(e) + } +} + +func main() { + log.Println(log.NORMAL, "--- Day 9: Smoke Basin ---") + timer.Start() + part1() + timer.Tick() + log.Println(log.NORMAL) + + part2() + timer.Tick() + log.Println(log.NORMAL) +} + +func part1() { + log.Println(log.NORMAL, "* Part 1 *") + log.Println(log.NORMAL, " Goal: Find all of the low points on your heightmap.") + log.Println(log.NORMAL, " Answer: What is the sum of the risk levels of all low points on your heightmap?") + + points, lines := loadPoints() + minPoints := findMinPoints(points) + + riskLevelSum := 0 + for _, p := range minPoints { + riskLevelSum += getRiskLevel(points, p) + } + + log.Printf(log.NORMAL, "Read %d lines from input\n", lines) + log.Printf(log.DIAGNOSTIC, "Min points are:") + for _, minPoint := range minPoints { + log.Printf(log.DIAGNOSTIC, " (%d, %d)", minPoint.col, minPoint.row) + } + log.Println(log.DIAGNOSTIC) + log.Printf(log.NORMAL, "The risk point total of all min points is %d\n", riskLevelSum) +} + +func part2() { + log.Println(log.NORMAL, "* Part 2 *") + log.Println(log.NORMAL, " Goal: Find the three largest basins.") + log.Println(log.NORMAL, " Answer: What do you get if you multiply together the sizes of the three largest basins?") + + points, lines := loadPoints() + minPoints := findMinPoints(points) + basins := getBasins(points, minPoints) + sort.Slice(basins, func(i, j int) bool { return basins[j].size < basins[i].size }) + + log.Printf(log.NORMAL, "Read %d lines from input\n", lines) + if len(basins) < 3 { + panic("expected to find at least 3 basins!") + } + log.Printf(log.NORMAL, "The three largest basins are") + for _, b := range basins[:3] { + log.Printf(log.NORMAL, " (%d, %d): %d", b.minPoint.col, b.minPoint.row, b.size) + } + log.Println(log.NORMAL) + totalValue := basins[0].size * basins[1].size * basins[2].size + log.Printf(log.NORMAL, "The multiplied value is %d\n", totalValue) +} + +func loadPoints() ([][]int, int) { + input, err := os.Open("./input.txt") + check(err) + defer input.Close() + + scanner := bufio.NewScanner(input) + lines := 0 + var points [][]int + + for scanner.Scan() { + // Read in a line and convert each character into an int in the array + values := arrays.MapStrToInt(strings.Split(scanner.Text(), ""), func(s string) int { i, _ := strconv.Atoi(s); return i }) + points = append(points, values) + log.Printf(log.DIAGNOSTIC, "Line %d: Read in %d values\n", lines+1, len(values)) + lines++ + } + + return points, lines +} + +type Point struct { + row, col int +} + +func findMinPoints(points [][]int) []Point { + var minPoints []Point + for rowIdx, row := range points { + for colIdx := range row { + if isMinPoint(points, rowIdx, colIdx) { + minPoints = append(minPoints, Point{rowIdx, colIdx}) + } + } + } + + return minPoints +} + +// check if the passed point actually exists within the list +func isPointValid(points [][]int, row, col int) bool { + if row < 0 || row >= len(points) { + return false + } + if col < 0 || col >= len(points[row]) { + return false + } + + return true +} + +// check if the current point is a minimum +func isMinPoint(points [][]int, row, col int) bool { + // it is a minimum point if all valid points around it have a higher value + value := points[row][col] + // LEFT + if isPointValid(points, row, col-1) && points[row][col-1] <= value { + return false + } + // RIGHT + if isPointValid(points, row, col+1) && points[row][col+1] <= value { + return false + } + // UP + if isPointValid(points, row-1, col) && points[row-1][col] <= value { + return false + } + // DOWN + if isPointValid(points, row+1, col) && points[row+1][col] <= value { + return false + } + + return true +} + +func getRiskLevel(points [][]int, point Point) int { + return 1 + points[point.row][point.col] +} + +type Basin struct { + minPoint Point + size int +} + +func getBasins(points [][]int, minPoints []Point) []Basin { + var basins []Basin + for _, minPoint := range minPoints { + size := getBasinSize(points, minPoint) + basins = append(basins, Basin{minPoint, size}) + } + + return basins +} + +func getBasinSize(points [][]int, minPoint Point) int { + // to calculate the size of a basin + // * start at the min point + basinPoints := []Point{minPoint} + + // * recursively check all adjacent points + expandBasin(&basinPoints, minPoint, points) + + // * if the point is LESS THAN 9, NOT OFF THE EDGE, and NOT ALREADY IN THE BAISN + // * then add it to the basin and recursively keep going! + return len(basinPoints) +} + +// returns whether the point should be added to a basin +// note: this is a naive and doesn't check that the point should actually exist in the given basin but rather if it meets a few criteria +func addToBasin(allPoints [][]int, basinPoints []Point, row, col int) bool { + // only add valid points + if !isPointValid(allPoints, row, col) { + return false + } + // that are not already in the list + if contains(basinPoints, Point{row, col}) { + return false + } + // and have a value less than 9 + return allPoints[row][col] < 9 +} + +func contains(points []Point, point Point) bool { + for _, p := range points { + if p.row == point.row && p.col == point.col { + return true + } + } + return false +} + +func expandBasin(basinPoints *[]Point, fromPoint Point, allPoints [][]int) { + // LEFT + if addToBasin(allPoints, *basinPoints, fromPoint.row, fromPoint.col-1) { + newPoint := Point{fromPoint.row, fromPoint.col - 1} + *basinPoints = append(*basinPoints, newPoint) + expandBasin(basinPoints, newPoint, allPoints) + } + // RIGHT + if addToBasin(allPoints, *basinPoints, fromPoint.row, fromPoint.col+1) { + newPoint := Point{fromPoint.row, fromPoint.col + 1} + *basinPoints = append(*basinPoints, newPoint) + expandBasin(basinPoints, newPoint, allPoints) + } + // UP + if addToBasin(allPoints, *basinPoints, fromPoint.row-1, fromPoint.col) { + newPoint := Point{fromPoint.row - 1, fromPoint.col} + *basinPoints = append(*basinPoints, newPoint) + expandBasin(basinPoints, newPoint, allPoints) + } + // DOWN + if addToBasin(allPoints, *basinPoints, fromPoint.row+1, fromPoint.col) { + newPoint := Point{fromPoint.row + 1, fromPoint.col} + *basinPoints = append(*basinPoints, newPoint) + expandBasin(basinPoints, newPoint, allPoints) + } +} diff --git a/day-9/main_test.go b/day-9/main_test.go new file mode 100644 index 0000000..860c1e8 --- /dev/null +++ b/day-9/main_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "AoC2021/utils/assert" + "testing" +) + +func TestDay9(t *testing.T) { + assert.IsTrue(t, true, "cool") + assert.EqualsStr(t, "foo", "bar", "baz") +} diff --git a/main_test.go.template b/main_test.go.template index ec00cc1..73fd0bc 100644 --- a/main_test.go.template +++ b/main_test.go.template @@ -7,5 +7,5 @@ import ( func TestDay#DAY#(t *testing.T) { assert.IsTrue(t, true, "cool") - assert.StrEquals(t, "foo", "bar", "baz") + assert.EqualsStr(t, "foo", "bar", "baz") } diff --git a/utils/arrays/arrays.go b/utils/arrays/arrays.go index 5c2713c..0fa0cb7 100644 --- a/utils/arrays/arrays.go +++ b/utils/arrays/arrays.go @@ -47,3 +47,11 @@ func MapStrToInt(input []string, fn func(string) int) []int { } return retVals } + +func Reverse(input []string) []string { + var output []string + for idx := len(input) - 1; idx >= 0; idx-- { + output = append(output, input[idx]) + } + return output +}