diff --git a/.gitignore b/.gitignore index c1ef9ab..13ae7a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ input +output node_modules go.work.sum \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..45a9a7a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + } + ] +} diff --git a/2015/go/go.mod b/2015/go/go.mod index 7582625..f8123f2 100644 --- a/2015/go/go.mod +++ b/2015/go/go.mod @@ -1,5 +1,5 @@ module aoc-2015 -go 1.21.5 +go 1.23.4 require gonum.org/v1/gonum v0.14.0 diff --git a/2016/go/day22/example-input b/2016/go/day22/example-input new file mode 100644 index 0000000..ea3c224 --- /dev/null +++ b/2016/go/day22/example-input @@ -0,0 +1,6 @@ +root@ebhq-gridcenter# df -h +Filesystem Size Used Avail Use% +/dev/grid/node-x0-y0 88T 67T 21T 76% +/dev/grid/node-x0-y1 85T 73T 12T 85% +/dev/grid/node-x0-y2 94T 73T 21T 77% +/dev/grid/node-x0-y3 91T 72T 19T 79% \ No newline at end of file diff --git a/2016/go/day22/main.go b/2016/go/day22/main.go new file mode 100644 index 0000000..0e2099d --- /dev/null +++ b/2016/go/day22/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "fmt" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + parseDisks(contents) + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 22, + Part: 1, + Value: "TODO", + }) +} + +func partTwo(contents []string) { + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 22, + Part: 2, + Value: "TODO", + }) +} + +type disk struct { + totalSize int + used int +} + +func parseDisks(contents []string) { + var disks [][]disk + + for i, line := range contents { + var x, y, totalSize, used int + if i <= 1 { + continue + } + // Parse line from eg '/dev/grid/node-x0-y0 88T 67T 21T 76%' Size Used Avail Use%' using regex + re := regexp.MustCompile(`/dev/grid/node-x(\d+)-y(\d+)\s+(\d+)T\s+(\d+)T\s+(\d+)T\s+\d+%`) + matches := re.FindStringSubmatch(line) + + x, _ = strconv.Atoi(matches[1]) + y, _ = strconv.Atoi(matches[2]) + totalSize, _ = strconv.Atoi(matches[2]) + used, _ = strconv.Atoi(matches[3]) + + // if + + fmt.Println(matches) + } +} + +func (d disk) free() int { + return d.totalSize - d.used +} diff --git a/2016/go/go.mod b/2016/go/go.mod index 1e27aa4..4a95b6b 100644 --- a/2016/go/go.mod +++ b/2016/go/go.mod @@ -1,3 +1,3 @@ module aoc-2016 -go 1.21.5 +go 1.23.4 diff --git a/2024/go/day01/example-input b/2024/go/day01/example-input new file mode 100644 index 0000000..dfca0b1 --- /dev/null +++ b/2024/go/day01/example-input @@ -0,0 +1,6 @@ +3 4 +4 3 +2 5 +1 3 +3 9 +3 3 \ No newline at end of file diff --git a/2024/go/day01/main.go b/2024/go/day01/main.go new file mode 100644 index 0000000..9453303 --- /dev/null +++ b/2024/go/day01/main.go @@ -0,0 +1,115 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "math" + "os" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + combos := parseInput(contents) + + sort.Slice(combos[0], func(i, j int) bool { + return combos[0][i] < combos[0][j] + }) + + sort.Slice(combos[1], func(i, j int) bool { + return combos[1][i] < combos[1][j] + }) + + difference := int64(0) + + for i := 0; i < len(combos[0]); i++ { + difference += int64(math.Abs(float64(combos[0][i] - combos[1][i]))) + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 1, + Part: 1, + Value: difference, + }) +} + +func partTwo(contents []string) { + combos := parseInput(contents) + + sort.Slice(combos[0], func(i, j int) bool { + return combos[0][i] < combos[0][j] + }) + + sort.Slice(combos[1], func(i, j int) bool { + return combos[1][i] < combos[1][j] + }) + + similarityScore := int64(0) + + for i := 0; i < len(combos[0]); i++ { + // We go through all of the lhs list, and multiply the number by the qnty in the rhs list + qty := int64(0) + currVal := combos[0][i] + + for j := 0; j < len(combos[1]); j++ { + if combos[1][j] > currVal { + break + } + if combos[1][j] == currVal { + qty++ + } + } + + similarityScore += qty * currVal + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 1, + Part: 2, + Value: similarityScore, + }) +} + +func parseInput(contents []string) [2][]int64 { + leftCombos := make([]int64, 0) + rightCombos := make([]int64, 0) + + for _, line := range contents { + var numOne, numTwo int64 + + tmp := strings.Split(line, " ") + + numOne, _ = strconv.ParseInt(tmp[0], 10, 64) + numTwo, _ = strconv.ParseInt(tmp[1], 10, 64) + + leftCombos = append(leftCombos, numOne) + rightCombos = append(rightCombos, numTwo) + } + + return [2][]int64{leftCombos, rightCombos} +} diff --git a/2024/go/day02/example-input b/2024/go/day02/example-input new file mode 100644 index 0000000..82cd679 --- /dev/null +++ b/2024/go/day02/example-input @@ -0,0 +1,6 @@ +7 6 4 2 1 +1 2 7 8 9 +9 7 6 2 1 +1 3 2 4 5 +8 6 4 4 1 +1 3 6 7 9 \ No newline at end of file diff --git a/2024/go/day02/main.go b/2024/go/day02/main.go new file mode 100644 index 0000000..de04dde --- /dev/null +++ b/2024/go/day02/main.go @@ -0,0 +1,136 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "math" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + reports := parseInput(contents) + + safeReports := make([][]int, 0) + + for _, line := range reports { + if isSafe, _ := passes(line); isSafe { + safeReports = append(safeReports, line) + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 2, + Part: 1, + Value: len(safeReports), + }) +} + +func partTwo(contents []string) { + reports := parseInput(contents) + + safeReports := make([][]int, 0) + // reportIndexesToCheck := make([]int, 0) + + for _, line := range reports { + // Check if pass, then check where it failed (and also zero); + // isSafe, failureIndex := passes(line) + // if isSafe { + // safeReports = append(safeReports, line) + // } else if isNowSafe, _ := passes(removeAtIndex(line, failureIndex)); isNowSafe { + // safeReports = append(safeReports, line) + // } else if isNowSafe, _ := passes(removeAtIndex(line, 0)); isNowSafe { + // safeReports = append(safeReports, line) + // } + + // Brute force each one as checking just the failure doesn't seem to work (must be some edge cases) + if isSafe, _ := passes(line); isSafe { + safeReports = append(safeReports, line) + } else { + for i := 0; i < len(line); i++ { + newReport := removeAtIndex(line, i) + if isNowSafe, _ := passes(newReport); isNowSafe { + safeReports = append(safeReports, line) + break + } + } + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 2, + Part: 2, + Value: len(safeReports), + }) +} + +func passes(report []int) (bool, int) { + prevDiff := 0 + for i := 1; i < len(report); i++ { + diff := report[i] - report[i-1] + + // If diff > 3, then we skip this line + if math.Abs(float64(diff)) > 3 || + math.Abs(float64(diff)) < 1 || + diff == 0 || + (prevDiff < 0 && diff > 0) || + (prevDiff > 0 && diff < 0) { + return false, i + } + + prevDiff = diff + } + return true, 0 +} + +func parseInput(contents []string) [][]int { + output := make([][]int, 0) + + for _, line := range contents { + tmp := strings.Fields(line) + lineOutput := []int{} + + for i := 0; i < len(tmp); i++ { + tmp, err := strconv.Atoi(tmp[i]) + if err != nil { + panic(err) + } + lineOutput = append(lineOutput, tmp) + } + output = append(output, lineOutput) + } + + return output +} + +func removeAtIndex(s []int, index int) []int { + clone := make([]int, len(s)) + copy(clone, s) + return append(clone[:index], clone[index+1:]...) +} diff --git a/2024/go/day03/example-input b/2024/go/day03/example-input new file mode 100644 index 0000000..2e1a90a --- /dev/null +++ b/2024/go/day03/example-input @@ -0,0 +1 @@ +xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5)) \ No newline at end of file diff --git a/2024/go/day03/main.go b/2024/go/day03/main.go new file mode 100644 index 0000000..d3b950f --- /dev/null +++ b/2024/go/day03/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "fmt" + "os" + "path/filepath" + "regexp" + "runtime" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var contents, _ = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents string) { + multiplier := 0 + + re := regexp.MustCompile(`mul\(\d{1,3},\d{1,3}\)`) + matches := re.FindAllString(contents, -1) + for _, match := range matches { + var valOne, valTwo int + _, err := fmt.Sscanf(match, "mul(%d,%d)", &valOne, &valTwo) + if err != nil { + panic(err) + } + fmt.Println(valOne, valTwo) + multiplier += valOne * valTwo + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 3, + Part: 1, + Value: multiplier, + }) +} + +func partTwo(contents string) { + multiplier := 0 + addEnabled := true + + re := regexp.MustCompile(`(mul\(\d{1,3},\d{1,3}\))|(do\(\))|(don't\(\))`) + matches := re.FindAllString(contents, -1) + for _, match := range matches { + if match == "do()" { + addEnabled = true + continue + } else if match == "don't()" { + addEnabled = false + continue + } + + if addEnabled { + var valOne, valTwo int + _, err := fmt.Sscanf(match, "mul(%d,%d)", &valOne, &valTwo) + if err != nil { + panic(err) + } + fmt.Println(valOne, valTwo) + multiplier += valOne * valTwo + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 3, + Part: 2, + Value: multiplier, + }) +} diff --git a/2024/go/day04/example-input b/2024/go/day04/example-input new file mode 100644 index 0000000..c41c5ea --- /dev/null +++ b/2024/go/day04/example-input @@ -0,0 +1,10 @@ +MMMSXXMASM +MSAMXMSMSA +AMXSXMAAMM +MSAMASMSMX +XMASAMXAMM +XXAMMXXAMA +SMSMSASXSS +SAXAMASAAA +MAMMMXMMMM +MXMXAXMASX \ No newline at end of file diff --git a/2024/go/day04/main.go b/2024/go/day04/main.go new file mode 100644 index 0000000..91ce862 --- /dev/null +++ b/2024/go/day04/main.go @@ -0,0 +1,140 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + xmasCount := 0 + // Go through the grid, and for every X we encounter, check for the word Xmas + for i, line := range contents { + for j := range line { + if contents[i][j] == 'X' { + qty := checkForXmas(i, j, &contents) + xmasCount += qty + } + } + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 4, + Part: 1, + Value: xmasCount, + }) +} + +func partTwo(contents []string) { + crossMasCount := 0 + // Go through the grid, and for every X we encounter, check for the word Xmas + for i, line := range contents { + for j := range line { + if contents[i][j] == 'A' && checkForCrossMas(i, j, &contents) { + crossMasCount++ + } + } + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 4, + Part: 2, + Value: crossMasCount, + }) +} + +func checkForXmas(i int, j int, contents *[]string) int { + // deifne the steps - probably a better way to do this just with [-1,0,1] but this isnt too long to write + directions := [8][2]int{ + {1, 0}, + {-1, 0}, + {0, 1}, + {0, -1}, + {1, 1}, + {1, -1}, + {-1, 1}, + {-1, -1}, + } + + stepsToCharMap := map[int]byte{ + 1: 'M', + 2: 'A', + 3: 'S', + } + + matches := 0 + + for _, direction := range directions { + // Check we've not exceeded the range for the current direction + if direction[0]*3+i > len((*contents)[0])-1 || + direction[0]*3+i < 0 || + direction[1]*3+j > len((*contents))-1 || + direction[1]*3+j < 0 { + continue + } + + isAMatch := true + // Otherwise, check for 'MAS' with 3 intervals: + for n := 1; n <= 3; n++ { + if (*contents)[i+(direction[0]*n)][j+(direction[1]*n)] != stepsToCharMap[n] { + isAMatch = false + break + } + } + + if isAMatch { + matches++ + } + } + + return matches + +} + +func checkForCrossMas(i int, j int, contents *[]string) bool { + // Manually check this time as fewer cases + + // First check boundaries are not exceeded + if i-1 < 0 || j-1 < 0 || j+1 > len(*contents)-1 || i+1 > len(*contents)-1 { + // Out of range this way + return false + } + + // first check left diag - \ + if ((*contents)[i-1][j-1] == 'M' && (*contents)[i+1][j+1] == 'S') || ((*contents)[i-1][j-1] == 'S' && (*contents)[i+1][j+1] == 'M') { + //safe to continue if here - mainly using the else + } else { + return false + } + + // next check right diag - / + if ((*contents)[i-1][j+1] == 'M' && (*contents)[i+1][j-1] == 'S') || ((*contents)[i-1][j+1] == 'S' && (*contents)[i+1][j-1] == 'M') { + return true + } + + return false + +} diff --git a/2024/go/day05/example-input b/2024/go/day05/example-input new file mode 100644 index 0000000..d4d4441 --- /dev/null +++ b/2024/go/day05/example-input @@ -0,0 +1,28 @@ +47|53 +97|13 +97|61 +97|47 +75|29 +61|13 +75|53 +29|13 +97|29 +53|29 +61|53 +97|53 +61|29 +47|13 +75|47 +97|75 +47|61 +75|61 +47|29 +75|13 +53|13 + +75,47,61,53,29 +97,61,53,29,13 +75,29,13 +75,97,47,61,53 +61,13,29 +97,13,75,29,47 \ No newline at end of file diff --git a/2024/go/day05/main.go b/2024/go/day05/main.go new file mode 100644 index 0000000..86d0175 --- /dev/null +++ b/2024/go/day05/main.go @@ -0,0 +1,214 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "reflect" + "runtime" + "sort" + "strconv" + "strings" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +type Instruction [2]int +type Update []int + +func partOne(contents []string) { + instructions, updates := parseInput(contents) + + middleNumSum := 0 + + for _, update := range updates { + isValid := true + for _, instruction := range instructions { + firstPageNum := getIndexOfInt(update, instruction[0]) + if firstPageNum == -1 { + // Not exists so can skip + continue + } + + secondPageNum := getIndexOfInt(update, instruction[1]) + + if secondPageNum == -1 { + continue + } + + if secondPageNum < firstPageNum { + isValid = false + break + } + + } + + if isValid { + middleNumSum += update[(len(update)-1)/2] // Assume that there's no even ones... + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 5, + Part: 1, + Value: middleNumSum, + }) +} + +func partTwo(contents []string) { + instructions, updates := parseInput(contents) + + invalidUpdates := make([]Update, 0) + + for _, update := range updates { + isValid := true + for _, instruction := range instructions { + firstPageNum := getIndexOfInt(update, instruction[0]) + if firstPageNum == -1 { + // Not exists so can skip + continue + } + + secondPageNum := getIndexOfInt(update, instruction[1]) + + if secondPageNum == -1 { + continue + } + + if secondPageNum < firstPageNum { + isValid = false + break + } + } + + if !isValid { + invalidUpdates = append(invalidUpdates, update) + } + } + + middleNumSum := 0 + for _, update := range invalidUpdates { + update := bruteForceUntilValid(update, instructions) + sort.Slice(update, func(i, j int) bool { + return getSortScore(update, i, instructions) < getSortScore(update, j, instructions) + }) + // Assume all have the ability to be sorted correctly; + middleNumSum += update[(len(update)-1)/2] // Assume that there's no even ones... + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 5, + Part: 2, + Value: middleNumSum, + }) +} + +func parseInput(contents []string) ([]Instruction, []Update) { + isInstruction := true + instructions := make([]Instruction, 0) + updates := make([]Update, 0) + + for _, line := range contents { + if line == "" { + isInstruction = false + continue + } + + if isInstruction { + var inOne, inTwo int + + temp := strings.Split(line, "|") + inOne, _ = strconv.Atoi(temp[0]) + inTwo, _ = strconv.Atoi(temp[1]) + + instructions = append(instructions, [2]int{inOne, inTwo}) + continue + } + + temp := strings.Split(line, ",") + ints := make(Update, len(temp)) + + for i := range temp { + ints[i], _ = strconv.Atoi(temp[i]) + } + + updates = append(updates, ints) + } + + return instructions, updates +} + +func getIndexOfInt(array []int, val int) int { + for i := 0; i < len(array); i++ { + if array[i] == val { + return i + } + } + return -1 +} + +func getSortScore(update Update, index int, instructions []Instruction) int { + // Find all the rules where it's on the left OR right, and for each time it's on the left, add 0, for the right, add one + val := update[index] + sortScore := 0 + for _, instruction := range instructions { + if instruction[1] == val { + sortScore++ + } + } + + return sortScore +} + +func bruteForceUntilValid(update Update, instructions []Instruction) Update { + isValid := true + + for { + for _, instruction := range instructions { + firstPageNum := getIndexOfInt(update, instruction[0]) + if firstPageNum == -1 { + // Not exists so can skip + continue + } + + secondPageNum := getIndexOfInt(update, instruction[1]) + + if secondPageNum == -1 { + continue + } + + if secondPageNum < firstPageNum { + swapF := reflect.Swapper(update) + swapF(firstPageNum, secondPageNum) + + update = bruteForceUntilValid(update, instructions) + } + } + + if isValid { + return update + } + } +} diff --git a/2024/go/day06/example-input b/2024/go/day06/example-input new file mode 100644 index 0000000..b60e466 --- /dev/null +++ b/2024/go/day06/example-input @@ -0,0 +1,10 @@ +....#..... +.........# +.......... +..#....... +.......#.. +.......... +.#..^..... +........#. +#......... +......#... \ No newline at end of file diff --git a/2024/go/day06/main.go b/2024/go/day06/main.go new file mode 100644 index 0000000..c2b55db --- /dev/null +++ b/2024/go/day06/main.go @@ -0,0 +1,180 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + startPosition := determineStartIndex(contents) + visitedPositions := map[[2]int]bool{ + startPosition: true, + } + + currDirectionIndex := 0 + directionsInOrder := [4][2]int{ + {-1, 0}, // up + {0, 1}, // right + {1, 0}, // down + {0, -1}, // left + } + + currPosition := startPosition + + for { + // Take step in current direction; + newPosition := [2]int{ + currPosition[0] + directionsInOrder[currDirectionIndex][0], + currPosition[1] + directionsInOrder[currDirectionIndex][1], + } + + // Have we finished? + if newPosition[0] < 0 || newPosition[0] > len(contents)-1 || newPosition[1] < 0 || newPosition[1] > len(contents[0])-1 { + break + } + + // Have we encountered a '#' + if contents[newPosition[0]][newPosition[1]] == '#' { + // If we have, change direction and go again! + currDirectionIndex = (currDirectionIndex + 1) % 4 + continue + } + + // Otherwise, we're free to take a step! + visitedPositions[newPosition] = true + currPosition = newPosition + + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 6, + Part: 1, + Value: len(visitedPositions), + }) +} + +func partTwo(contents []string) { + obstructionPositions := make(map[[2]int]bool, 0) + + for i := 0; i < len(contents); i++ { + for j := 0; j < len(contents[i]); j++ { + if contents[i][j] == '#' { + continue // Already has one so cant be here + } + + if contents[i][j] == '^' { + continue + } + + testContents := make([]string, len(contents)) + + copy(testContents, contents) + testContents[i] = replaceAtIndex(testContents[i], '#', j) + + if testIfRepeat(testContents) { + obstructionPositions[[2]int{i, j}] = true + } + } + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 6, + Part: 2, + Value: len(obstructionPositions), + }) + +} + +func testIfRepeat(contents []string) bool { + startPosition := determineStartIndex(contents) + + currDirectionIndex := 0 + directionsInOrder := [4][2]int{ + {-1, 0}, // up + {0, 1}, // right + {1, 0}, // down + {0, -1}, // left + } + + visitedPositionAndDirection := map[[3]int]bool{ + {startPosition[0], startPosition[1], currDirectionIndex}: true, + } + + currPosition := startPosition + steps := 0 + for { + // Take step in current direction; + newPosition := [2]int{ + currPosition[0] + directionsInOrder[currDirectionIndex][0], + currPosition[1] + directionsInOrder[currDirectionIndex][1], + } + + // Have we finished? + if newPosition[0] < 0 || newPosition[0] > len(contents)-1 || newPosition[1] < 0 || newPosition[1] > len(contents[0])-1 { + return false + } + + // Have we encountered a '#' + if contents[newPosition[0]][newPosition[1]] == '#' { + // If we have, change direction and go again! + currDirectionIndex = (currDirectionIndex + 1) % 4 + continue + } + + // Otherwise, we're free to take a step! + currPosition = newPosition + steps++ + + // have we been here before? + _, ok := visitedPositionAndDirection[[3]int{currPosition[0], currPosition[1], currDirectionIndex}] + // If the key exists + if ok { + return true + } + + visitedPositionAndDirection[[3]int{currPosition[0], currPosition[1], currDirectionIndex}] = true + } +} + +func determineStartIndex(contents []string) [2]int { + for i, line := range contents { + for j := 0; j < len(line); j++ { + if contents[i][j] == '^' { + return [2]int{i, j} + } + } + } + + return [2]int{-1, -1} +} + +func replaceAtIndex(in string, r rune, i int) string { + out := []rune(in) + out[i] = r + return string(out) +} diff --git a/2024/go/day07/example-input b/2024/go/day07/example-input new file mode 100644 index 0000000..87b8b25 --- /dev/null +++ b/2024/go/day07/example-input @@ -0,0 +1,9 @@ +190: 10 19 +3267: 81 40 27 +83: 17 5 +156: 15 6 +7290: 6 8 6 15 +161011: 16 10 13 +192: 17 8 14 +21037: 9 7 18 13 +292: 11 6 16 20 \ No newline at end of file diff --git a/2024/go/day07/main.go b/2024/go/day07/main.go new file mode 100644 index 0000000..9eb0db1 --- /dev/null +++ b/2024/go/day07/main.go @@ -0,0 +1,123 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +type Calibration struct { + result int64 + testValues []int64 +} + +func partOne(contents []string) { + calibrations := parseInput(contents) + var totalValidResult int64 + + for _, calibration := range calibrations { + if recursiveSolve(&calibration, 0, 0, false) { + totalValidResult += calibration.result + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 7, + Part: 1, + Value: totalValidResult, + }) +} + +func partTwo(contents []string) { + calibrations := parseInput(contents) + var totalValidResult int64 + + for _, calibration := range calibrations { + if recursiveSolve(&calibration, 0, 0, true) { + totalValidResult += calibration.result + } + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 7, + Part: 2, + Value: totalValidResult, + }) +} + +func recursiveSolve(calibration *Calibration, currValue int64, index int, isPartTwo bool) bool { + if index == len(calibration.testValues) { + return currValue == calibration.result + } + + // Otherwise, define the possibilities for the next index + return recursiveSolve(calibration, currValue*calibration.testValues[index], index+1, isPartTwo) || + recursiveSolve(calibration, currValue+calibration.testValues[index], index+1, isPartTwo) || + (isPartTwo && recursiveSolve(calibration, concatenateThenInt(currValue, calibration.testValues[index]), index+1, true)) + +} + +func parseInput(contents []string) []Calibration { + calibrations := make([]Calibration, 0) + for _, line := range contents { + temp := strings.Split(line, ": ") + + result, _ := strconv.ParseInt(temp[0], 10, 64) + + strValues := strings.Fields(temp[1]) + intVals := make([]int64, 0) + for _, val := range strValues { + intVal, _ := strconv.ParseInt(val, 10, 64) + intVals = append(intVals, intVal) + } + + calibrations = append(calibrations, Calibration{ + result: result, + testValues: intVals, + }) + } + + return calibrations + +} + +func concatenateThenInt(intOne int64, intTwo int64) int64 { + strOne := strconv.FormatInt(intOne, 10) + strTwo := strconv.FormatInt(intTwo, 10) + + // Now concatenate; + res := strOne + strTwo + + // then convert and return + intResult, err := strconv.ParseInt(res, 10, 64) + if err != nil { + panic(err) + } + return intResult +} diff --git a/2024/go/day08/example-input b/2024/go/day08/example-input new file mode 100644 index 0000000..de0f909 --- /dev/null +++ b/2024/go/day08/example-input @@ -0,0 +1,12 @@ +............ +........0... +.....0...... +.......0.... +....0....... +......A..... +............ +............ +........A... +.........A.. +............ +............ \ No newline at end of file diff --git a/2024/go/day08/main.go b/2024/go/day08/main.go new file mode 100644 index 0000000..0443c3d --- /dev/null +++ b/2024/go/day08/main.go @@ -0,0 +1,210 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + // First go through and build a map of coordinates by char; + coordinatesByChar := make(map[byte][][2]int, 0) + + antiNodesMap := make(map[[2]int]bool, 0) + + for i := 0; i < len(contents); i++ { + for j := 0; j < len(contents[i]); j++ { + if contents[i][j] != '.' { + coordinatesByChar[contents[i][j]] = append(coordinatesByChar[contents[i][j]], [2]int{i, j}) + } + } + } + + // Now brute force each grid point to check if we satisfy the criteria; + for i := 0; i < len(contents); i++ { + for j := 0; j < len(contents[i]); j++ { + testNode := [2]int{i, j} + isValid := false + + // have we been here before? + _, ok := antiNodesMap[testNode] + // If the key exists + if ok { + continue // Already been confirmed as an antenna + } + + // Otherwise, check whether it could work as an antinode + // eg. test node 1,3: 3,4 is 2,1 | 5,5 is 4,2 which is double, and diff between 2,1 5,5 is 4,2 + for char, coords := range coordinatesByChar { + if char == contents[i][j] { + continue // Skip for the character type where there is a node here (but not for others) + } + // The difference between test coord and coord A must equal the diff between coord A cna coord B... maybe? + for n := 0; n < len(coords); n++ { + for m := 0; m < len(coords); m++ { + if n == m { + continue + } + + diffCoordAAndTest := [2]int{ + coords[n][0] - testNode[0], + coords[n][1] - testNode[1], + } + + diffCoordBAndCoordA := [2]int{ + coords[m][0] - coords[n][0], + coords[m][1] - coords[n][1], + } + + if diffCoordAAndTest[0] == diffCoordBAndCoordA[0] && diffCoordAAndTest[1] == diffCoordBAndCoordA[1] { + isValid = true + break + } + } + } + } + if isValid { + antiNodesMap[testNode] = true + } + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 8, + Part: 1, + Value: len(antiNodesMap), + }) +} + +func partTwo(contents []string) { + // First go through and build a map of coordinates by char; + coordinatesByChar := make(map[byte][][2]int, 0) + + antiNodesMap := make(map[[2]int]bool, 0) + + for i := 0; i < len(contents); i++ { + for j := 0; j < len(contents[i]); j++ { + if contents[i][j] != '.' { + coordinatesByChar[contents[i][j]] = append(coordinatesByChar[contents[i][j]], [2]int{i, j}) + } + } + } + + // Now brute force each grid point to check if we satisfy the criteria; + for i := 0; i < len(contents); i++ { + for j := 0; j < len(contents[i]); j++ { + testNode := [2]int{i, j} + isValid := false + + // have we been here before? + _, ok := antiNodesMap[testNode] + // If the key exists + if ok { + continue // Already been confirmed as an antenna + } + + // Otherwise, check whether it could work as an antinode + // eg. test node 1,3: 3,4 is 2,1 | 5,5 is 4,2 which is double, and diff between 2,1 5,5 is 4,2 + for char, coords := range coordinatesByChar { + if char == contents[i][j] { + isValid = true + break + } + // The difference between test coord and coord A must equal a MULTIPLE OF the diff between coord A and coord B... maybe? + for n := 0; n < len(coords); n++ { + for m := 0; m < len(coords); m++ { + if n == m { + continue + } + + diffCoordBAndCoordA := [2]int{ + coords[m][0] - coords[n][0], + coords[m][1] - coords[n][1], + } + + if isOnPath(testNode, diffCoordBAndCoordA, coords[n], contents) { + isValid = true + break + } + } + } + } + if isValid { + antiNodesMap[testNode] = true + } + } + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 8, + Part: 2, + Value: len(antiNodesMap), + }) +} + +func isOnPath(testNode [2]int, diffCoordBAndCoordA [2]int, targetCoord [2]int, contents []string) bool { + // Go through multiples until we exceed the range or have found it! + probeCoord := [2]int{testNode[0], testNode[1]} + multiplier := 1 + outOfRangeCount := 0 + + for { + outOfRangeCount = 0 + addition := [2]int{ + multiplier * diffCoordBAndCoordA[0], + multiplier * diffCoordBAndCoordA[1], + } + + probeCoordOne := [2]int{ + addition[0] + probeCoord[0], + addition[1] + probeCoord[1], + } + + probeCoordTwo := [2]int{ + (-1 * addition[0]) + probeCoord[0], + (-1 * addition[1]) + probeCoord[1], + } + if probeCoordOne[0] < 0 || probeCoordOne[0] > len(contents)-1 || probeCoordOne[1] < 0 || probeCoordOne[1] > len(contents[0])-1 { + outOfRangeCount++ + } + + if probeCoordTwo[0] < 0 || probeCoordTwo[0] > len(contents)-1 || probeCoordTwo[1] < 0 || probeCoordTwo[1] > len(contents[0])-1 { + outOfRangeCount++ + } + + if outOfRangeCount == 2 { + return false + } + + if (probeCoordOne[0] == targetCoord[0] && probeCoordOne[1] == targetCoord[1]) || + (probeCoordTwo[0] == targetCoord[0] && probeCoordTwo[1] == targetCoord[1]) { + return true + } + + multiplier++ + } +} diff --git a/2024/go/day09/example-input b/2024/go/day09/example-input new file mode 100644 index 0000000..5ff5aae --- /dev/null +++ b/2024/go/day09/example-input @@ -0,0 +1 @@ +2333133121414131402 \ No newline at end of file diff --git a/2024/go/day09/main.go b/2024/go/day09/main.go new file mode 100644 index 0000000..1c12955 --- /dev/null +++ b/2024/go/day09/main.go @@ -0,0 +1,217 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" + "strconv" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var contents, _ = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +type FileBlock struct { + id int64 + size int64 + freeSpace int64 +} + +func partOne(contents string) { + fileBlocks := parseInput(contents) + + // Instead of building a very long string with dots in, we'll build an array which each contains the multiplier and ID + // index 0 is ID index 1 is qty + reorderedFiles := make([][2]int64, 0) + for i := 0; i < len(fileBlocks); i++ { + fileBlock := fileBlocks[i] + // Go upwards and whenever we reach a free space, look for the LAST element + reorderedFiles = append(reorderedFiles, [2]int64{fileBlock.id, fileBlock.size}) + + if fileBlock.freeSpace > 0 { + toAdd := takeFromEnd(fileBlock.id, fileBlock.freeSpace, &fileBlocks) + reorderedFiles = append(reorderedFiles, toAdd...) + } + } + + sum := int64(0) + currentPosition := int64(0) + for i := 0; i < len(reorderedFiles); i++ { + for j := 0; j < int(reorderedFiles[i][1]); j++ { + sum += currentPosition * reorderedFiles[i][0] + currentPosition++ + } + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 9, + Part: 1, + Value: sum, + }) +} + +func takeFromEnd(currentFileId int64, freeSpace int64, fileBlocks *[]FileBlock) [][2]int64 { + returnVal := make([][2]int64, 0) + if freeSpace == 0 { + return returnVal + } + + for j := len(*fileBlocks) - 1; j > 0; j-- { + if freeSpace == 0 { + break + } + // Start from the last and see if we can build this array, working downwards until we find one with size remaining + fileBlock := (*fileBlocks)[j] + + if currentFileId == fileBlock.id { + break + } + + if fileBlock.size > 0 { + amountUsed := int64(0) + if fileBlock.size <= freeSpace { + // All of it + amountUsed = fileBlock.size + } else { + amountUsed = freeSpace + } + + returnVal = append(returnVal, [2]int64{fileBlock.id, amountUsed}) + (*fileBlocks)[j].size = fileBlock.size - amountUsed + freeSpace -= amountUsed + } + } + + return returnVal +} + +type posSize struct { + pos int64 + size int64 +} + +func partTwo(contents string) { + files := make(map[int64]posSize, 0) + blanks := make([]posSize, 0) // IDX 0 pos, IDX 1 length + currentId := int64(0) + currentPosition := int64(0) + for i := 0; i < len(contents); i = i + 2 { + size, err := strconv.ParseInt(string(contents[i]), 10, 64) + if err != nil { + panic(err) + } + lastIndex := i + 1 + freeSpace := int64(0) + if i+1 < len(contents) { + freeSpace, err = strconv.ParseInt(string(contents[lastIndex]), 10, 64) + } + if err != nil { + panic(err) + } + + files[currentId] = posSize{currentPosition, size} + blanks = append(blanks, posSize{currentPosition + size, freeSpace}) + + currentId++ + currentPosition += size + freeSpace + } + + for { + currentId-- + if currentId < 0 { + break + } + + file := files[currentId] + + // Find the next blank; + for i := 0; i < len(blanks); i++ { + blank := blanks[i] + + if blank.pos >= file.pos { + // RHS of it so we dont care + break + } + + if blank.size >= file.size { + files[currentId] = posSize{blank.pos, file.size} + if blank.size == file.size { + // Same size - delete the blank + blanks = removeAtIndex(blanks, i) + } else { + // Larger - shrink the available space + blanks[i].pos += file.size // move position along + blanks[i].size -= file.size // drop size + } + break + } + } + } + + sum := int64(0) + for fileId, file := range files { + for n := file.pos; n < file.pos+file.size; n++ { + sum += n * fileId + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 9, + Part: 2, + Value: sum, + }) +} + +func parseInput(contents string) []FileBlock { + fileBlocks := make([]FileBlock, 0) + currentId := int64(0) + for i := 0; i < len(contents); i = i + 2 { + size, err := strconv.ParseInt(string(contents[i]), 10, 64) + if err != nil { + panic(err) + } + lastIndex := i + 1 + freeSpace := int64(0) + if i+1 < len(contents) { + freeSpace, err = strconv.ParseInt(string(contents[lastIndex]), 10, 64) + } + if err != nil { + panic(err) + } + + fileBlocks = append(fileBlocks, FileBlock{ + id: currentId, + size: size, + freeSpace: freeSpace, + }) + currentId++ + } + + return fileBlocks +} + +func removeAtIndex(s []posSize, index int) []posSize { + clone := make([]posSize, len(s)) + copy(clone, s) + return append(clone[:index], clone[index+1:]...) +} diff --git a/2024/go/day10/example-input b/2024/go/day10/example-input new file mode 100644 index 0000000..7bb1248 --- /dev/null +++ b/2024/go/day10/example-input @@ -0,0 +1,8 @@ +89010123 +78121874 +87430965 +96549874 +45678903 +32019012 +01329801 +10456732 \ No newline at end of file diff --git a/2024/go/day10/main.go b/2024/go/day10/main.go new file mode 100644 index 0000000..17dfcad --- /dev/null +++ b/2024/go/day10/main.go @@ -0,0 +1,160 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" + "strconv" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + grid := parseInput(contents) + scoresSum := 0 + + // Go through each point and if it's a 0, do a DFS on it + for i, row := range grid { + for j, point := range row { + if point == 0 { + scoresSum += determineScoreForPoint(&grid, i, j, false) + } + } + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 10, + Part: 1, + Value: scoresSum, + }) +} + +type queueStruct struct { + pos [2]int + currVal int +} + +func partTwo(contents []string) { + grid := parseInput(contents) + scoresSum := 0 + + // Go through each point and if it's a 0, do a DFS on it + for i, row := range grid { + for j, point := range row { + if point == 0 { + scoresSum += determineScoreForPoint(&grid, i, j, true) + } + } + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 10, + Part: 2, + Value: scoresSum, + }) +} + +func determineScoreForPoint(grid *[][]int, i int, j int, isPartTwo bool) int { + numOfNines := 0 + visited := make(map[[2]int]bool, 0) + directions := [4][2]int{ + {1, 0}, + {-1, 0}, + {0, 1}, + {0, -1}, + } + currentValue := (*grid)[i][j] + + queue := make([]queueStruct, 0) + + queue = append(queue, queueStruct{ + pos: [2]int{i, j}, + currVal: currentValue, + }) + + for { + if len(queue) == 0 { + break + } + + // Grab the last value from the queue + element := queue[len(queue)-1] + queue = queue[:len(queue)-1] + + // if visited, for part 1 we exit. For part 2, we are counting the total number of ways to reach 9, so we can overlap + if !isPartTwo { + _, ok := visited[element.pos] + if ok { + continue + } + } + + // otherwise, mark as visited! + visited[element.pos] = true + + if element.currVal == 9 { + // We've made it, can increment and abort! + numOfNines++ + continue + } + + // Now expand out in all 4 directions and see if valid, add to queue if so + for _, dir := range directions { + newI := element.pos[0] + dir[0] + newJ := element.pos[1] + dir[1] + + // Out of bounds checks + if newI < 0 || newJ < 0 || newI > len(*grid)-1 || newJ > len((*grid)[0])-1 { + continue + } + + // Rules check; it must increment 1 from the current Value: + if element.currVal != (*grid)[newI][newJ]-1 { + continue + } + + // Otherwise, we add to list + queue = append(queue, queueStruct{ + pos: [2]int{newI, newJ}, + currVal: (*grid)[newI][newJ], + }) + } + } + + return numOfNines +} + +func parseInput(contents []string) [][]int { + output := make([][]int, len(contents)) + for i, line := range contents { + lineInt := make([]int, len(line)) + for j, char := range line { + intVal, _ := strconv.Atoi(string(char)) + lineInt[j] = intVal + } + output[i] = lineInt + } + + return output +} diff --git a/2024/go/day11/example-input b/2024/go/day11/example-input new file mode 100644 index 0000000..528f9d5 --- /dev/null +++ b/2024/go/day11/example-input @@ -0,0 +1 @@ +125 17 \ No newline at end of file diff --git a/2024/go/day11/main.go b/2024/go/day11/main.go new file mode 100644 index 0000000..8e72d8b --- /dev/null +++ b/2024/go/day11/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var contents, _ = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +type cacheKey struct { + rock int64 + remainingBlinks int +} + +func partOne(contents string) { + rocks := parseInput(contents) + + cacheMap := make(map[cacheKey]int64, 0) + + numRocks := int64(0) + for _, rock := range rocks { + numRocks += getNumberOfRocks(rock, 25, &cacheMap) + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 11, + Part: 1, + Value: numRocks, + }) +} + +func partTwo(contents string) { + rocks := parseInput(contents) + + cacheMap := make(map[cacheKey]int64, 0) + + numRocks := int64(0) + for _, rock := range rocks { + numRocks += getNumberOfRocks(rock, 75, &cacheMap) + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 11, + Part: 2, + Value: numRocks, + }) +} + +func getNumberOfRocks(rock int64, blinksRemaining int, cacheMap *map[cacheKey]int64) int64 { + val, ok := (*cacheMap)[cacheKey{rock, blinksRemaining}] + if ok { + return val + } + + if blinksRemaining == 0 { + return 1 // Just this rock + } + + if rock == 0 { + value := getNumberOfRocks(int64(1), blinksRemaining-1, cacheMap) + (*cacheMap)[cacheKey{rock, blinksRemaining}] = value + return value + } + + stringVal := strconv.FormatInt(rock, 10) + stringValSize := len(stringVal) + if stringValSize%2 == 0 { + // Split down the middle; + var newRockOneStr, newRockTwoStr string + var newRockOne, newRockTwo int64 + // Special case if size is 2: + if stringValSize == 2 { + newRockOneStr = string(stringVal[0]) + newRockTwoStr = string(stringVal[1]) + } else { + newRockOneStr = stringVal[0:(stringValSize / 2)] + newRockTwoStr = stringVal[(stringValSize / 2):stringValSize] + } + + newRockOne, _ = strconv.ParseInt(newRockOneStr, 10, 64) + newRockTwo, _ = strconv.ParseInt(newRockTwoStr, 10, 64) + + value := getNumberOfRocks(newRockOne, blinksRemaining-1, cacheMap) + getNumberOfRocks(newRockTwo, blinksRemaining-1, cacheMap) + + (*cacheMap)[cacheKey{rock, blinksRemaining}] = value + + return value + } + + value := getNumberOfRocks(rock*2024, blinksRemaining-1, cacheMap) + (*cacheMap)[cacheKey{rock, blinksRemaining}] = value + return value + +} + +func parseInput(contents string) []int64 { + strValues := strings.Fields(contents) + + intVals := make([]int64, 0) + for _, val := range strValues { + intVal, _ := strconv.ParseInt(val, 10, 64) + intVals = append(intVals, intVal) + } + return intVals +} diff --git a/2024/go/day12/example-input b/2024/go/day12/example-input new file mode 100644 index 0000000..0b328f1 --- /dev/null +++ b/2024/go/day12/example-input @@ -0,0 +1,10 @@ +RRRRIICCFF +RRRRIICCCF +VVRRRCCFFF +VVRCCCJFFF +VVVVCJJCFE +VVIVCCJJEE +VVIIICJJEE +MIIIIIJJEE +MIIISIJEEE +MMMISSJEEE \ No newline at end of file diff --git a/2024/go/day12/main.go b/2024/go/day12/main.go new file mode 100644 index 0000000..4807ca7 --- /dev/null +++ b/2024/go/day12/main.go @@ -0,0 +1,201 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +type plotStruct struct { + letter byte + points [][2]int + perimeter int + sides int +} + +func partOne(contents []string) { + // Use a BFS and not just a loop; + plots := buildPlots(contents, false) + + totalPrice := 0 + for i := 0; i < len(plots); i++ { + totalPrice += (len(plots[i].points) * plots[i].perimeter) + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 12, + Part: 1, + Value: totalPrice, + }) +} + +func partTwo(contents []string) { + // Use a BFS and not just a loop; + plots := buildPlots(contents, true) + + totalPrice := 0 + for i := 0; i < len(plots); i++ { + totalPrice += (len(plots[i].points) * plots[i].sides) + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 12, + Part: 2, + Value: totalPrice, + }) +} + +type queueStruct struct { + pos [2]int + currChar byte +} + +func buildPlots(contents []string, partTwo bool) []plotStruct { + plots := make([]plotStruct, 0) + visited := make(map[[2]int]bool, 0) + + for i, line := range contents { + for j := range line { + if _, ok := visited[[2]int{i, j}]; ok { + continue + } + + plots = append(plots, buildPlot(i, j, contents, &visited, partTwo)) + } + } + + return plots +} + +func buildPlot(i int, j int, contents []string, visited *map[[2]int]bool, partTwo bool) plotStruct { + plot := plotStruct{} + + directions := [4][2]int{ + {1, 0}, // Down + {0, 1}, // Right + {-1, 0}, // Up + {0, -1}, // Left + } + + currentValue := contents[i][j] + plot.letter = currentValue + queue := make([]queueStruct, 0) + queue = append(queue, queueStruct{ + pos: [2]int{i, j}, + currChar: currentValue, + }) + + var element queueStruct + + for { + if len(queue) == 0 { + break + } + + // Grab the next element in queue + element, queue = queue[0], queue[1:] + + // if visited, exit + _, ok := (*visited)[element.pos] + if ok { + continue + } + + (*visited)[element.pos] = true + + plot.points = append(plot.points, element.pos) + + for _, dir := range directions { + newI := element.pos[0] + dir[0] + newJ := element.pos[1] + dir[1] + + // Out of bounds checks + if newI < 0 || newJ < 0 || newI > len(contents)-1 || newJ > len(contents[0])-1 { + plot.perimeter++ + continue + } + + // Rules check; it must be the same char + if plot.letter != contents[newI][newJ] { + plot.perimeter++ + continue + } + + queue = append(queue, queueStruct{ + pos: [2]int{newI, newJ}, + currChar: element.currChar, + }) + } + } + + if partTwo { + corners := 0 + // For part 2, we count sides. This is equivalent to counting corners which is easier + orthogonalPairs := [][2][2]int{} + for i := 0; i < 4; i++ { + orthogonalPairs = append(orthogonalPairs, [2][2]int{directions[i], directions[(i+1)%4]}) + } + for _, point := range plot.points { + cornerCount := 0 + for _, pair := range orthogonalPairs { + // To check fo a corner, we check that the either: At least one orthogonal direction pair is not in plot OR both match but diagonal is not in plot + posOne := [2]int{point[0] + pair[0][0], point[1] + pair[0][1]} + posTwo := [2]int{point[0] + pair[1][0], point[1] + pair[1][1]} + // 1. One orth. direction pair is not in plot + if !isInSlice(plot.points, posOne) && !isInSlice(plot.points, posTwo) { + cornerCount++ + continue + } + + //2. both match but diagonal is not in plot + diagonal := [2]int{point[0] + pair[0][0] + pair[1][0], point[1] + pair[0][1] + pair[1][1]} + if isInSlice(plot.points, posOne) && isInSlice(plot.points, posTwo) && !isInSlice(plot.points, diagonal) { + cornerCount++ + continue + } + + } + if cornerCount > 0 { + corners += cornerCount + } + } + + plot.sides = corners + } + + return plot +} + +func isInSlice(haystack [][2]int, needle [2]int) bool { + for _, point := range haystack { + if point == needle { + return true + } + } + return false +} diff --git a/2024/go/day13/example-input b/2024/go/day13/example-input new file mode 100644 index 0000000..444a287 --- /dev/null +++ b/2024/go/day13/example-input @@ -0,0 +1,15 @@ +Button A: X+94, Y+34 +Button B: X+22, Y+67 +Prize: X=8400, Y=5400 + +Button A: X+26, Y+66 +Button B: X+67, Y+21 +Prize: X=12748, Y=12176 + +Button A: X+17, Y+86 +Button B: X+84, Y+37 +Prize: X=7870, Y=6450 + +Button A: X+69, Y+23 +Button B: X+27, Y+71 +Prize: X=18641, Y=10279 \ No newline at end of file diff --git a/2024/go/day13/main.go b/2024/go/day13/main.go new file mode 100644 index 0000000..dccf2c5 --- /dev/null +++ b/2024/go/day13/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "fmt" + "math" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + equations := parseInput(contents) + + totalTickets := int64(0) + + // Now we have equations, use cramer's rule to determine X and Y (aka A btn presses and B btn presses) + for _, eqn := range equations { + countA := float64(((eqn.cOne * eqn.bTwo) - (eqn.bOne * eqn.cTwo))) / float64(((eqn.aOne * eqn.bTwo) - (eqn.bOne * eqn.aTwo))) + + if _, frac := math.Modf(countA); frac != 0 { + continue + } + countB := float64((eqn.cOne - eqn.aOne*int64(countA)) / eqn.bOne) + + if _, frac := math.Modf(countB); frac != 0 { + continue + } + + if countA > 0 && countA < 100 && countB > 0 && countB < 100 { + totalTickets += int64(countA*3 + countB) + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 13, + Part: 1, + Value: totalTickets, + }) +} + +func partTwo(contents []string) { + equations := parseInput(contents) + + totalTickets := int64(0) + + // Now we have equations, use cramer's rule to determine X and Y (aka A btn presses and B btn presses) + for _, eqn := range equations { + eqn.cOne += 10000000000000 + eqn.cTwo += 10000000000000 + countA := float64(((eqn.cOne * eqn.bTwo) - (eqn.bOne * eqn.cTwo))) / float64(((eqn.aOne * eqn.bTwo) - (eqn.bOne * eqn.aTwo))) + + if _, frac := math.Modf(countA); frac != 0 { + continue + } + countB := float64((eqn.cOne - eqn.aOne*int64(countA))) / float64(eqn.bOne) + + if _, frac := math.Modf(countB); frac != 0 { + continue + } + + if countA > 0 && countB > 0 { + totalTickets += int64(countA*3 + countB) + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 13, + Part: 2, + Value: totalTickets, + }) +} + +type equationsStruct struct { + aOne int64 + bOne int64 + cOne int64 + aTwo int64 + bTwo int64 + cTwo int64 +} + +func parseInput(contents []string) []equationsStruct { + equations := make([]equationsStruct, 0) + currentIndex := 0 + + for _, line := range contents { + if len(line) == 0 { + currentIndex++ + } else if line[:6] == "Button" { + if len(equations) == currentIndex { + // First time at this index so increment slice + equations = append(equations, equationsStruct{}) + } + + entries := regexp.MustCompile(`^Button (\w): X\+(\d+), Y\+(\d+)`) + match := entries.FindStringSubmatch(line) + + a, _ := strconv.ParseInt(match[2], 10, 64) + b, _ := strconv.ParseInt(match[3], 10, 64) + + if match[1] == "A" { + equations[currentIndex].aOne = a + equations[currentIndex].aTwo = b + } else { + equations[currentIndex].bOne = a + equations[currentIndex].bTwo = b + } + + } else if line[:5] == "Prize" { + var prizeX, prizeY int64 + fmt.Sscanf(line, "Prize: X=%d, Y=%d", &prizeX, &prizeY) + + equations[currentIndex].cOne = prizeX + equations[currentIndex].cTwo = prizeY + } + } + + return equations +} diff --git a/2024/go/day14/example-input b/2024/go/day14/example-input new file mode 100644 index 0000000..72a324a --- /dev/null +++ b/2024/go/day14/example-input @@ -0,0 +1,12 @@ +p=0,4 v=3,-3 +p=6,3 v=-1,-3 +p=10,3 v=-1,2 +p=2,0 v=2,-1 +p=0,0 v=1,3 +p=3,0 v=-2,-2 +p=7,6 v=-1,-3 +p=3,0 v=-1,-2 +p=9,3 v=2,3 +p=7,3 v=-1,2 +p=2,4 v=2,-3 +p=9,5 v=-3,-3 \ No newline at end of file diff --git a/2024/go/day14/main.go b/2024/go/day14/main.go new file mode 100644 index 0000000..e9c2bd6 --- /dev/null +++ b/2024/go/day14/main.go @@ -0,0 +1,235 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "bytes" + "fmt" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +type robotStruct struct { + position [2]int + velocity [2]int +} + +func partOne(contents []string) { + robots := parseInput(contents) + + columns := 101 + rows := 103 + if isUsingExample { + columns = 11 + rows = 7 + } + + // Now do a loop with a cache (as maybe will be cyclic?), of position and velocity vectors + cacheMap := make(map[robotStruct][2]int) + + seconds := 100 + + for i := 1; i <= seconds; i++ { + for j, robot := range robots { + // Shift it along! + + // First check if we've computed this before for this pos and vel? + if val, ok := cacheMap[robot]; ok { + robots[j].position = val + continue + } + + // Else let's do it and add to cache + newPosition := [2]int{ + ((robot.position[0] + robot.velocity[0]) + rows) % rows, + (robot.position[1] + robot.velocity[1] + columns) % columns, + } + + cacheMap[robot] = newPosition + robots[j].position = newPosition + + } + // displayMap(rows, columns, robots) + } + + robotsPerQuadrant := [4]int{} + for _, robot := range robots { + // Determine quadrant; + if robot.position[0] == (rows-1)/2 || robot.position[1] == (columns-1)/2 { + // Skip in in middle - easy case! + continue + } + + // Might be a better way to do this but it'll do! + if robot.position[0] < (rows-1)/2 && robot.position[1] < (columns-1)/2 { + robotsPerQuadrant[0]++ + } else if robot.position[0] < (rows-1)/2 && robot.position[1] > (columns-1)/2 { + robotsPerQuadrant[1]++ + } else if robot.position[0] > (rows-1)/2 && robot.position[1] < (columns-1)/2 { + robotsPerQuadrant[2]++ + } else if robot.position[0] > (rows-1)/2 && robot.position[1] > (columns-1)/2 { + robotsPerQuadrant[3]++ + } + } + + multiplier := 1 + for i := 0; i < len(robotsPerQuadrant); i++ { + multiplier *= robotsPerQuadrant[i] + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 14, + Part: 1, + Value: multiplier, + }) +} + +func partTwo(contents []string) { + robots := parseInput(contents) + + columns := 101 + rows := 103 + if isUsingExample { + columns = 11 + rows = 7 + } + + cacheMap := make(map[robotStruct][2]int) + + seconds := 10_000 // Try 10,000 + + // We'll output each second into a file ans then grep for when we have lots of robots in a line, hopefully this works + fo, err := os.Create("output") + if err != nil { + panic(err) + } + // close fo on exit and check for its returned error + defer func() { + if err := fo.Close(); err != nil { + panic(err) + } + }() + + for i := 1; i <= seconds; i++ { + for j, robot := range robots { + if val, ok := cacheMap[robot]; ok { + robots[j].position = val + continue + } + + newPosition := [2]int{ + ((robot.position[0] + robot.velocity[0]) + rows) % rows, + (robot.position[1] + robot.velocity[1] + columns) % columns, + } + + cacheMap[robot] = newPosition + robots[j].position = newPosition + + } + + grid := buildMap(rows, columns, robots) + outputMap(&grid, fo, i) + } + // Not really sure how to do this one... maybe just output + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 14, + Part: 2, + Value: "Run grep on the output and look for the tree! `grep 00000000000000000000 ./output -C 10`", + }) +} + +func parseInput(contents []string) []robotStruct { + robots := make([]robotStruct, 0) + re := regexp.MustCompile(`-?\d+`) + for _, line := range contents { + var posX, posY, velX, velY int + + matches := re.FindAllString(line, -1) + + posX, _ = strconv.Atoi(matches[0]) + posY, _ = strconv.Atoi(matches[1]) + velX, _ = strconv.Atoi(matches[2]) + velY, _ = strconv.Atoi(matches[3]) + + // Note X and Y are flipped to match matrix format and not cartesian coords! + robots = append(robots, robotStruct{ + position: [2]int{posY, posX}, + velocity: [2]int{velY, velX}, + }) + + } + + return robots +} + +func buildMap(rows int, columns int, robots []robotStruct) [][]byte { + grid := make([][]byte, 0) + + for i := 0; i < rows; i++ { + var builder bytes.Buffer + for j := 0; j < columns; j++ { + builder.WriteByte('.') + } + grid = append(grid, builder.Bytes()) + } + + for _, robot := range robots { + grid[robot.position[0]][robot.position[1]] = '0' + } + + return grid + +} + +func outputMap(grid *[][]byte, fo *os.File, seconds int) { + fo.WriteString("Seconds: " + strconv.Itoa(seconds)) + fo.WriteString("\n") + + for _, line := range *grid { + // write a chunk + if _, err := fo.Write(line); err != nil { + panic(err) + } + + fo.WriteString("\n") + } +} + +func displayMap(rows int, columns int, robots []robotStruct) { + grid := buildMap(rows, columns, robots) + + for i := 0; i < rows; i++ { + for j := 0; j < columns; j++ { + fmt.Print(string(grid[i][j])) + } + fmt.Println() + } + fmt.Println() + +} diff --git a/2024/go/day15/example-input b/2024/go/day15/example-input new file mode 100644 index 0000000..dfe6a69 --- /dev/null +++ b/2024/go/day15/example-input @@ -0,0 +1,9 @@ +####### +#...#.# +#.....# +#..OO@# +#..O..# +#.....# +####### + + 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +var DIRECTION_MAPPINGS = map[byte][2]int{ + '^': {-1, 0}, + 'v': {1, 0}, + '>': {0, 1}, + '<': {0, -1}, +} + +func partOne(contents []string) { + grid, instructions := parseInput(contents, false) + + found := false + + // Get start position; + startCoordinate := [2]int{} + for i := 0; i < len(grid); i++ { + for j := 0; j < len(grid[i]); j++ { + if grid[i][j] == '@' { + startCoordinate = [2]int{i, j} + found = true + break + } + } + if found { + break + } + } + + coordinate := startCoordinate + + // Now, go through the instructions; + for _, instruction := range instructions { + coordinate = moveRobotByInstruction(coordinate, instruction, &grid) + // fmt.Println(string(instruction)) + // displayGrid(&grid) + } + + // Now we compute the sum from the rock positions + gpsSum := 0 + for i, line := range grid { + for j := range line { + if grid[i][j] == 'O' { + gpsSum += 100*i + j + } + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 15, + Part: 1, + Value: gpsSum, + }) +} + +func partTwo(contents []string) { + grid, instructions := parseInput(contents, true) + + found := false + + // Get start position; + startCoordinate := [2]int{} + for i := 0; i < len(grid); i++ { + for j := 0; j < len(grid[i]); j++ { + if grid[i][j] == '@' { + startCoordinate = [2]int{i, j} + found = true + break + } + } + if found { + break + } + } + + coordinate := startCoordinate + + // Now, go through the instructions; + for _, instruction := range instructions { + coordinate = moveRobotByInstruction(coordinate, instruction, &grid) + // fmt.Println(string(instruction)) + // displayGrid(&grid) + } + + // Now we compute the sum from the rock positions + gpsSum := 0 + for i, line := range grid { + for j := range line { + if grid[i][j] == '[' { + gpsSum += 100*i + j + } + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 15, + Part: 2, + Value: gpsSum, + }) +} + +func moveRobotByInstruction(currentPosition [2]int, direction byte, grid *[][]byte) [2]int { + + movementMappings := make(map[[2]int]moveMapItem, 0) + if canMoveDirection(currentPosition, direction, grid, &movementMappings) { + newPosition := [2]int{ + currentPosition[0] + DIRECTION_MAPPINGS[direction][0], + currentPosition[1] + DIRECTION_MAPPINGS[direction][1], + } + + // First set them all to empty, and then fill them all in. Might be overkill, but it's not a huge operation so it'll do! + for _, mapItem := range movementMappings { + (*grid)[mapItem.oldPosition[0]][mapItem.oldPosition[1]] = '.' + } + + for newPos, mapItem := range movementMappings { + (*grid)[newPos[0]][newPos[1]] = mapItem.newChar + } + + return newPosition + } + + return currentPosition +} + +func canMoveDirection(currentPosition [2]int, direction byte, grid *[][]byte, movementMappings *map[[2]int]moveMapItem) bool { + newPosition := [2]int{ + currentPosition[0] + DIRECTION_MAPPINGS[direction][0], + currentPosition[1] + DIRECTION_MAPPINGS[direction][1], + } + + if (*grid)[newPosition[0]][newPosition[1]] == '#' { + // I think this covers the 'out of bounds' case and so we do not need 2 if statements c: + return false + } + + (*movementMappings)[newPosition] = moveMapItem{ + newChar: (*grid)[currentPosition[0]][currentPosition[1]], + oldPosition: currentPosition, + } + + // Part 1 only if statement! + if (*grid)[newPosition[0]][newPosition[1]] == 'O' { + // It's a boulder, so maybe we can move it? + return canMoveDirection(newPosition, direction, grid, movementMappings) + } + + // Part 2 only if statement! + if (*grid)[newPosition[0]][newPosition[1]] == '[' { + // It's a box, so maybe we can move it, need to now check 2 positions (unless we're going right) + if direction == '>' { + newLeftBracketPosition := [2]int{ + newPosition[0] + DIRECTION_MAPPINGS[direction][0], + newPosition[1] + DIRECTION_MAPPINGS[direction][1], + } + (*movementMappings)[newLeftBracketPosition] = moveMapItem{ + newChar: '[', + oldPosition: [2]int{currentPosition[0], currentPosition[1] + 1}, + } + return canMoveDirection([2]int{newPosition[0], newPosition[1] + 1}, direction, grid, movementMappings) + } + return canMoveDirection(newPosition, direction, grid, movementMappings) && + canMoveDirection([2]int{newPosition[0], newPosition[1] + 1}, direction, grid, movementMappings) + } + + // Part 2 only if statement! + if (*grid)[newPosition[0]][newPosition[1]] == ']' { + // It's a box, so maybe we can move it, need to now check 2 positions (unless we're going left) + if direction == '<' { + newRightBracketPosition := [2]int{ + newPosition[0] + DIRECTION_MAPPINGS[direction][0], + newPosition[1] + DIRECTION_MAPPINGS[direction][1], + } + (*movementMappings)[newRightBracketPosition] = moveMapItem{ + newChar: ']', + oldPosition: [2]int{currentPosition[0], currentPosition[1] - 1}, + } + return canMoveDirection([2]int{newPosition[0], newPosition[1] - 1}, direction, grid, movementMappings) + } + return canMoveDirection(newPosition, direction, grid, movementMappings) && + canMoveDirection([2]int{newPosition[0], newPosition[1] - 1}, direction, grid, movementMappings) + } + + // Only other case is it's safe to move! + return true +} + +type moveMapItem struct { + newChar byte + oldPosition [2]int +} + +func parseInput(contents []string, isPartTwo bool) ([][]byte, []byte) { + grid := make([][]byte, 0) + var builder bytes.Buffer + + isBuildingGrid := true // first par of input is the grid + + for _, line := range contents { + if isBuildingGrid { + if len(line) == 0 { + isBuildingGrid = false + continue + } + + if isPartTwo { + var gridBuilder bytes.Buffer + + for i := 0; i < len(line); i++ { + if line[i] == '.' || line[i] == '#' { + gridBuilder.WriteByte(line[i]) + gridBuilder.WriteByte(line[i]) + } else if line[i] == '@' { + gridBuilder.WriteByte('@') + gridBuilder.WriteByte('.') + } else if line[i] == 'O' { + gridBuilder.WriteByte('[') + gridBuilder.WriteByte(']') + } + } + + grid = append(grid, gridBuilder.Bytes()) + } else { + grid = append(grid, []byte(line)) + } + } else { + builder.WriteString(line) + } + } + + return grid, builder.Bytes() + +} + +func getColumn(grid *[][]byte, jIndex int) []byte { + column := make([]byte, 0) + + for i := 0; i < len(*grid); i++ { + column = append(column, (*grid)[i][jIndex]) + } + + return column +} + +func displayGrid(grid *[][]byte) { + for _, line := range *grid { + for _, char := range line { + fmt.Print(string(char)) + } + fmt.Println() + } +} diff --git a/2024/go/day16/example-input b/2024/go/day16/example-input new file mode 100644 index 0000000..54f4cd7 --- /dev/null +++ b/2024/go/day16/example-input @@ -0,0 +1,17 @@ +################# +#...#...#...#..E# +#.#.#.#.#.#.#.#.# +#.#.#.#...#...#.# +#.#.#.#.###.#.#.# +#...#.#.#.....#.# +#.#.#.#.#.#####.# +#.#...#.#.#.....# +#.#.#####.#.###.# +#.#.#.......#...# +#.#.###.#####.### +#.#.#...#.....#.# +#.#.#.#####.###.# +#.#.#.........#.# +#.#.#.#########.# +#S#.............# +################# \ No newline at end of file diff --git a/2024/go/day16/main.go b/2024/go/day16/main.go new file mode 100644 index 0000000..bb2cee8 --- /dev/null +++ b/2024/go/day16/main.go @@ -0,0 +1,297 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "math" + "os" + "path/filepath" + "runtime" + "sort" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + lowestPoints := bfs(contents) + + // BFS today I think! + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 16, + Part: 1, + Value: lowestPoints, + }) +} + +func partTwo(contents []string) { + totalSpotsOnAnyPath := bfsPt2(contents, bfs(contents)) + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 16, + Part: 2, + Value: totalSpotsOnAnyPath, + }) +} + +type queueStruct struct { + pos [2]int + currPoints int64 + currDirectionIndex int +} + +type cacheKey struct { + pos [2]int + dirIndex int +} + +var directions = [4][2]int{ + {1, 0}, // Down + {0, 1}, // Right + {-1, 0}, // Up + {0, -1}, // Left +} + +func bfs(contents []string) int64 { + queue := make([]queueStruct, 0) + visited := make(map[cacheKey]bool) + start, end := findStartAndEnd(contents) + + queue = append(queue, queueStruct{ + pos: start, + currPoints: int64(0), + currDirectionIndex: 1, // Starting facing right according to input + }) + + var element queueStruct + + for { + if len(queue) == 0 { + break + } + + // Take the element with the lowest points from the queue (should be the first) + element, queue = queue[0], queue[1:] + + if element.pos == end { + return element.currPoints + } + + // If visited, then we've been here with less points and so can skip + if _, ok := visited[cacheKey{element.pos, element.currDirectionIndex}]; ok { + continue + } + + visited[cacheKey{element.pos, element.currDirectionIndex}] = true + + // We only can go 90 degrees or stay the same, so just do 3 indexes: + for i := -1; i <= 1; i++ { + newDirIndex := (element.currDirectionIndex + i + 4) % 4 + dir := directions[newDirIndex] + + newI := element.pos[0] + dir[0] + newJ := element.pos[1] + dir[1] + + // Out of bounds checks - we can actually just check for a '#' symbol as we are surrounded by walls + if contents[newI][newJ] == '#' { + continue + } + + // Otherwise, we make the move and add to queue + if i == 0 { + // Just stepped forward - 1 point added; + queue = append(queue, queueStruct{ + pos: [2]int{newI, newJ}, + currPoints: element.currPoints + 1, + currDirectionIndex: element.currDirectionIndex, + }) + } else { + // Turned and stepped - 1000 + 1 point added + queue = append(queue, queueStruct{ + pos: [2]int{newI, newJ}, + currPoints: element.currPoints + 1001, + currDirectionIndex: newDirIndex, + }) + } + + } + + // Sort the queue by points (might be expensive but enables BFS so probably worth it?): + // Sort by lowest cost and then go upwards + sort.Slice(queue, func(i, j int) bool { + return queue[i].currPoints < queue[j].currPoints + }) + } + + return math.MaxInt64 +} + +type queueStructPt2 struct { + pos [2]int + currPoints int64 + currDirectionIndex int + visitedPoints map[[2]int]bool +} + +func bfsPt2(contents []string, bestPoints int64) int { + queue := make([]queueStructPt2, 0) + visitedBestScore := make(map[cacheKey]int64) + start, end := findStartAndEnd(contents) + visitedPoints := map[[2]int]bool{} + + allShortestPathCoordinates := make(map[[2]int]bool) + + queue = append(queue, queueStructPt2{ + pos: start, + currPoints: int64(0), + currDirectionIndex: 1, // Starting facing right according to input + visitedPoints: visitedPoints, + }) + + var element queueStructPt2 + + for { + if len(queue) == 0 { + break + } + + if element.currPoints > bestPoints { + // If it's exceeded the shortest path's score, we can drop this path + continue + } + + // Take the element with the lowest points from the queue (should be the first) + element, queue = queue[0], queue[1:] + + // If visited, then we've been here with less points and so can skip + if points, ok := visitedBestScore[cacheKey{element.pos, element.currDirectionIndex}]; ok && points < element.currPoints { + continue + } + + element.visitedPoints[element.pos] = true + + visitedBestScore[cacheKey{element.pos, element.currDirectionIndex}] = element.currPoints + + if element.pos == end { + addToShortestPathCoordinates(&allShortestPathCoordinates, element.visitedPoints) + continue + } + + // We only can go 90 degrees or stay the same, so just do 3 indexes: + for i := -1; i <= 1; i++ { + newDirIndex := (element.currDirectionIndex + i + 4) % 4 + dir := directions[newDirIndex] + + newI := element.pos[0] + dir[0] + newJ := element.pos[1] + dir[1] + + // Out of bounds checks - we can actually just check for a '#' symbol as we are surrounded by walls + if contents[newI][newJ] == '#' { + continue + } + + // Otherwise, we make the move and add to queue + if i == 0 { + // Have we been here with less score previously? + if points, ok := visitedBestScore[cacheKey{[2]int{newI, newJ}, element.currDirectionIndex}]; ok && points < element.currPoints+1 { + continue + } + + // Would this take it out of range? + if element.currPoints+1 > bestPoints { + continue + } + // Just stepped forward - 1 point added; + queue = append(queue, queueStructPt2{ + pos: [2]int{newI, newJ}, + currPoints: element.currPoints + 1, + currDirectionIndex: element.currDirectionIndex, + visitedPoints: copyMap(element.visitedPoints), + }) + } else { + // Have we been here with less score previously? + if points, ok := visitedBestScore[cacheKey{[2]int{newI, newJ}, newDirIndex}]; ok && points < element.currPoints+1001 { + continue + } + + // Would this take it out of range? + if element.currPoints+1001 > bestPoints { + continue + } + // Turned and stepped; 1000 + 1 point added + queue = append(queue, queueStructPt2{ + pos: [2]int{newI, newJ}, + currPoints: element.currPoints + 1001, + currDirectionIndex: newDirIndex, + visitedPoints: copyMap(element.visitedPoints), + }) + } + + } + + // Sort the queue by points (might be expensive but enables BFS so probably worth it?): + // Sort by lowest cost and then go upwards + sort.Slice(queue, func(i, j int) bool { + return queue[i].currPoints < queue[j].currPoints + }) + } + + return len(allShortestPathCoordinates) +} + +func addToShortestPathCoordinates(visited *map[[2]int]bool, newVisitedCoordinates map[[2]int]bool) { + for coord := range newVisitedCoordinates { + (*visited)[coord] = true + } +} + +func findStartAndEnd(contents []string) ([2]int, [2]int) { + var start, end [2]int + foundCount := 0 + for i, line := range contents { + for j, char := range line { + if char == 'S' { + start = [2]int{i, j} + foundCount++ + } + if char == 'E' { + end = [2]int{i, j} + foundCount++ + } + } + if foundCount == 2 { + break + } + } + + return start, end +} + +func copyMap(m map[[2]int]bool) map[[2]int]bool { + m2 := make(map[[2]int]bool, len(m)) + var id [2]int + for id = range m { + m2[id] = true + } + return m2 +} diff --git a/2024/go/day17/example-input b/2024/go/day17/example-input new file mode 100644 index 0000000..b92f31c --- /dev/null +++ b/2024/go/day17/example-input @@ -0,0 +1,5 @@ +Register A: 18427963 +Register B: 0 +Register C: 0 + +Program: 2,4,1,1,7,5,0,3,4,3,1,6,5,5,3,0 \ No newline at end of file diff --git a/2024/go/day17/main.go b/2024/go/day17/main.go new file mode 100644 index 0000000..49b74ae --- /dev/null +++ b/2024/go/day17/main.go @@ -0,0 +1,242 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "math" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + registers, instructions := parseInput(contents) + + instructionPointer := 0 + output := make([]int64, 0) + + // Go through the instructions; + for { + if instructionPointer > len(instructions)-1 { + break + } + + handleInstruction(®isters, instructions[instructionPointer], instructions[instructionPointer+1], &instructionPointer, &output) + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 17, + Part: 1, + Value: sliceToString(output), + }) + +} + +// I don't really understand this part, and have crafted the solution based on HyperNeutrino's video. TODO - come back to this later. +func partTwo(contents []string) { + _, instructions := parseInput(contents) + + A := find(instructions, 0) + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 17, + Part: 2, + Value: A, + }) +} + +func find(instructions []int64, ans int64) int64 { + if len(instructions) == 0 { + return ans + } + + // Recursively go through the instructions + for i := int64(0); i < 8; i++ { + var A, B, C int64 + A = (ans << 3) + i + B = A % 8 + B = B ^ 1 + C = A >> B + // A = A >> 3 // Not a clue why we have to comment this out, but it's zero otherwise... Maybe because we care about the input of a, not how it changes? + B = B ^ C + B = B ^ 6 + if B%8 == instructions[len(instructions)-1] { + sub := find(instructions[:len(instructions)-1], A) + if sub > 0 { + return sub + } + } + } + return 0 +} + +func sliceToString(slice []int64) string { + var builder strings.Builder + + for i, outputVal := range slice { + stringVal := strconv.FormatInt(outputVal, 10) + builder.WriteString(stringVal) + if i != len(slice)-1 { + builder.WriteByte(',') + } + } + + return builder.String() +} + +func handleInstruction(registers *map[byte]int64, opcode int64, operand int64, pointer *int, output *[]int64) { + if opcode == 0 { + // (*registers)['A'] = (*registers)['A'] >> getComboOperand(operand, registers) // Apparently this is the same! + numerator := (*registers)['A'] + denominator := math.Pow(2, float64(getComboOperand(operand, registers))) + + result := math.Floor(float64(numerator) / float64(denominator)) + (*registers)['A'] = int64(result) + *pointer = *pointer + 2 + return + } + if opcode == 1 { + result := (*registers)['B'] ^ operand + (*registers)['B'] = result + *pointer = *pointer + 2 + return + } + if opcode == 2 { + (*registers)['B'] = getComboOperand(operand, registers) % 8 + *pointer = *pointer + 2 + return + } + if opcode == 3 { + if (*registers)['A'] == 0 { + *pointer = *pointer + 2 + return + } + *pointer = int(operand) + return + } + if opcode == 4 { + result := (*registers)['B'] ^ (*registers)['C'] + (*registers)['B'] = result + *pointer = *pointer + 2 + return + } + if opcode == 5 { + outputVal := getComboOperand(operand, registers) % 8 + *pointer = *pointer + 2 + *output = append(*output, outputVal) + return + } + if opcode == 6 { + numerator := (*registers)['A'] + denominator := math.Pow(2, float64(getComboOperand(operand, registers))) + + result := math.Floor(float64(numerator) / float64(denominator)) + (*registers)['B'] = int64(result) + *pointer = *pointer + 2 + return + } + if opcode == 7 { + numerator := (*registers)['A'] + denominator := math.Pow(2, float64(getComboOperand(operand, registers))) + + result := math.Floor(float64(numerator) / float64(denominator)) + (*registers)['C'] = int64(result) + *pointer = *pointer + 2 + return + } else { + panic("Invalid opcode?") + } +} + +func getComboOperand(operand int64, register *map[byte]int64) int64 { + if operand <= 3 { + return operand + } + + if operand == 4 { + return (*register)['A'] + } + + if operand == 5 { + return (*register)['B'] + } + + if operand == 6 { + return (*register)['C'] + } + + panic("Invalid!") +} + +func parseInput(contents []string) (map[byte]int64, []int64) { + registers := make(map[byte]int64) + instructions := make([]int64, 0) + + for _, line := range contents { + if len(line) == 0 { + continue + } + + if line[:8] == "Register" { + register := line[9] + val := line[12:] + + intVal, _ := strconv.ParseInt(val, 10, 64) + + registers[register] = intVal + } + + if line[:7] == "Program" { + stringInstructions := strings.Split(line[9:], ",") + for i := range stringInstructions { + intVal, _ := strconv.ParseInt(stringInstructions[i], 10, 64) + instructions = append(instructions, intVal) + } + } + } + + return registers, instructions +} + +func outputForA(A int64, instructions []int64, originalRegisters map[byte]int64) []int64 { + instructionPointer := 0 + output := make([]int64, 0) + registers := make(map[byte]int64) + registers['A'] = A + registers['B'] = originalRegisters['B'] + registers['C'] = originalRegisters['C'] + for { + if instructionPointer > len(instructions)-1 { + break + } + + handleInstruction(®isters, instructions[instructionPointer], instructions[instructionPointer+1], &instructionPointer, &output) + } + + return output +} diff --git a/2024/go/day18/example-input b/2024/go/day18/example-input new file mode 100644 index 0000000..0371b23 --- /dev/null +++ b/2024/go/day18/example-input @@ -0,0 +1,25 @@ +5,4 +4,2 +4,5 +3,0 +2,1 +6,3 +2,4 +1,5 +0,6 +3,3 +2,6 +5,1 +1,2 +5,5 +2,5 +6,5 +1,4 +0,4 +6,4 +1,1 +6,1 +1,0 +0,5 +1,6 +2,0 \ No newline at end of file diff --git a/2024/go/day18/main.go b/2024/go/day18/main.go new file mode 100644 index 0000000..5421dae --- /dev/null +++ b/2024/go/day18/main.go @@ -0,0 +1,189 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "fmt" + "math" + "os" + "path/filepath" + "runtime" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +type queueStruct struct { + pos [2]int + steps int +} + +func partOne(contents []string) { + instructions := parseInput(contents) + + nanoSeconds := 1024 + gridSize := 71 + if isUsingExample { + nanoSeconds = 12 + gridSize = 7 + } + + grid := buildGrid(instructions, nanoSeconds, gridSize) + + // displayGrid(grid) + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 18, + Part: 1, + Value: bfs(instructions, grid), + }) +} + +func bfs(instructions [][2]int, grid [][]byte) int { + // BFS now! + queue := make([]queueStruct, 0) + visited := make(map[[2]int]bool) + + directions := [4][2]int{ + {1, 0}, // Down + {0, 1}, // Right + {-1, 0}, // Up + {0, -1}, // Left + } + + queue = append(queue, queueStruct{ + pos: [2]int{0, 0}, + steps: 0, + }) + + var element queueStruct + + for { + if len(queue) == 0 { + break + } + + // Grab the next element in queue + element, queue = queue[0], queue[1:] + + // if visited, skip + _, ok := visited[element.pos] + if ok { + continue + } + + if element.pos == [2]int{len(grid) - 1, len(grid) - 1} { + return element.steps + } + + visited[element.pos] = true + + for _, dir := range directions { + newI := element.pos[0] + dir[0] + newJ := element.pos[1] + dir[1] + + // Out of bounds checks + if newI < 0 || newJ < 0 || newI > len(grid)-1 || newJ > len(grid)-1 { + continue + } + + if grid[newI][newJ] == '#' { + continue + } + + queue = append(queue, queueStruct{ + pos: [2]int{newI, newJ}, + steps: element.steps + 1, + }) + } + } + + return math.MaxInt +} + +func buildGrid(instructions [][2]int, nanoseconds int, gridSize int) [][]byte { + grid := make([][]byte, 0) + for i := 0; i < gridSize; i++ { + grid = append(grid, make([]byte, 0)) + for j := 0; j < gridSize; j++ { + grid[i] = append(grid[i], '.') + } + } + + for t := 0; t < nanoseconds; t++ { + instruction := instructions[t] + grid[instruction[0]][instruction[1]] = '#' + } + + return grid +} + +func partTwo(contents []string) { + instructions := parseInput(contents) + + nanoSeconds := 1024 + gridSize := 71 + if isUsingExample { + nanoSeconds = 12 + gridSize = 7 + } + + grid := buildGrid(instructions, nanoSeconds, gridSize) + + nsWhenBreaks := math.MaxInt + + for i := nanoSeconds + 1; i < len(instructions)-1; i++ { + grid[instructions[i][0]][instructions[i][1]] = '#' // Block the new coordinate spot! + if steps := bfs(instructions, grid); steps == math.MaxInt { + nsWhenBreaks = i + break + } + } + + coordinate := fmt.Sprintf("%d,%d", instructions[nsWhenBreaks][1], instructions[nsWhenBreaks][0]) + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 18, + Part: 2, + Value: coordinate, + }) +} + +func parseInput(contents []string) [][2]int { + coordinates := make([][2]int, 0) + for _, line := range contents { + var rowCoord, colCoord int + fmt.Sscanf(line, "%d,%d", &colCoord, &rowCoord) + coordinates = append(coordinates, [2]int{rowCoord, colCoord}) + } + + return coordinates +} + +func displayGrid(grid [][]byte) { + for _, row := range grid { + for _, char := range row { + fmt.Print(string(char)) + } + fmt.Println("") + } +} diff --git a/2024/go/day19/example-input b/2024/go/day19/example-input new file mode 100644 index 0000000..ad43a74 --- /dev/null +++ b/2024/go/day19/example-input @@ -0,0 +1,10 @@ +r, wr, b, g, bwu, rb, gb, br + +brwrr +bggr +gbbr +rrbgbr +ubwu +bwurrg +brgr +bbrgwb \ No newline at end of file diff --git a/2024/go/day19/main.go b/2024/go/day19/main.go new file mode 100644 index 0000000..f8a33ef --- /dev/null +++ b/2024/go/day19/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" + "strings" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + availableTowels, patterns := parseInput(contents) + + possibleDesigns := 0 + + cacheMap := make(map[string]bool, 0) + + for _, pattern := range patterns { + if canMakeDesign(availableTowels, pattern, &cacheMap) { + possibleDesigns++ + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 19, + Part: 1, + Value: possibleDesigns, + }) +} + +func canMakeDesign(availableTowels map[string]bool, pattern string, cacheMap *map[string]bool) bool { + if len(pattern) == 0 { + return true + } + + if val, ok := (*cacheMap)[pattern]; ok { + return val + } + + for towel := range availableTowels { + if len(towel) > len(pattern) { + continue // This towel is too big. Maybe we could drop from future recursions if slow performance + } + + // Does this towel work as the next slot? + if towel == pattern[:len(towel)] { + // This is valid, so we can recursively determine if this towel is a valid one for this step + newPattern := pattern[len(towel):] + isValid := canMakeDesign(availableTowels, newPattern, cacheMap) + (*cacheMap)[newPattern] = isValid + if isValid { + return true + } + } + } + + return false +} + +func partTwo(contents []string) { + availableTowels, patterns := parseInput(contents) + + numDesignsForPattern := make(map[string]int, 0) + + totalWaysToMakeDesign := 0 + for _, pattern := range patterns { + totalWaysToMakeDesign += howManyWaysToMakeDesign(availableTowels, pattern, &numDesignsForPattern) + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 19, + Part: 2, + Value: totalWaysToMakeDesign, + }) +} + +func howManyWaysToMakeDesign(availableTowels map[string]bool, pattern string, numDesignsForPattern *map[string]int) int { + if len(pattern) == 0 { + return 1 + } + + if val, ok := (*numDesignsForPattern)[pattern]; ok { + return val + } + totalNumWays := 0 + for towel := range availableTowels { + if len(towel) > len(pattern) { + continue // This towel is too big. Maybe we could drop from future recursions if slow performance + } + + // Does this towel work as the next slot? + if towel == pattern[:len(towel)] { + // This is valid, so we can recursively determine if this towel is a valid one for this step + newPattern := pattern[len(towel):] + numWays := howManyWaysToMakeDesign(availableTowels, newPattern, numDesignsForPattern) + (*numDesignsForPattern)[newPattern] = numWays + totalNumWays += numWays + } + } + + return totalNumWays +} + +func parseInput(contents []string) (map[string]bool, []string) { + availableTowels := make(map[string]bool, 0) + patterns := make([]string, 0) + for i, line := range contents { + if len(line) == 0 { + continue + } + if i == 0 { + values := strings.Fields(line) + for j, value := range values { + if j != len(values)-1 { + availableTowels[value[:len(value)-1]] = true // Remove comma + } else { + availableTowels[value] = true + } + } + + continue + } + // Parse sets of patterns here + patterns = append(patterns, line) + } + + return availableTowels, patterns +} diff --git a/2024/go/day20/example-input b/2024/go/day20/example-input new file mode 100644 index 0000000..f107d40 --- /dev/null +++ b/2024/go/day20/example-input @@ -0,0 +1,15 @@ +############### +#...#...#.....# +#.#.#.#.#.###.# +#S#...#.#.#...# +#######.#.#.### +#######.#.#...# +#######.#.###.# +###..E#...#...# +###.#######.### +#...###...#...# +#.#####.#.###.# +#.#...#.#.#...# +#.#.#.#.#.#.### +#...#...#...### +############### \ No newline at end of file diff --git a/2024/go/day20/main.go b/2024/go/day20/main.go new file mode 100644 index 0000000..d200c55 --- /dev/null +++ b/2024/go/day20/main.go @@ -0,0 +1,266 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "fmt" + "math" + "os" + "path/filepath" + "runtime" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + grid := parseToGrid(contents) + distancesToPoint := make([][]int, len(grid)) + for i := 0; i < len(grid); i++ { + distancesToPoint[i] = make([]int, len(grid[i])) + for j := 0; j < len(grid[i]); j++ { + distancesToPoint[i][j] = -1 + } + } + + start, end := findStartAndEnd(contents) + distancesToPoint[start[0]][start[1]] = 0 + bfsDetermineDistances(grid, start, end, &distancesToPoint) + + count := 0 + + // To avoid double counting, we only look at half the points per space; + halfDistances := [4][2]int{ + {2, 0}, + {1, 1}, + {0, 2}, + {-1, 1}, + } + + picoSecondsToSave := 100 + if isUsingExample { + picoSecondsToSave = 40 // Expect 2 as the output + } + + for i := 0; i < len(grid); i++ { + for j := 0; j < len(grid[i]); j++ { + if distancesToPoint[i][j] == -1 { + continue + } + + for _, dir := range halfDistances { + newI := i + dir[0] + newJ := j + dir[1] + + // Out of bounds checks + if newI < 0 || newJ < 0 || newI > len(grid)-1 || newJ > len(grid[0])-1 { + continue + } + + if grid[newI][newJ] == '#' { + continue + } + + if int(math.Abs(float64(distancesToPoint[i][j]-distancesToPoint[newI][newJ]))) >= picoSecondsToSave+2 { + count++ + } + } + + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 20, + Part: 1, + Value: count, + }) +} + +func partTwo(contents []string) { + grid := parseToGrid(contents) + distancesToPoint := make([][]int, len(grid)) + for i := 0; i < len(grid); i++ { + distancesToPoint[i] = make([]int, len(grid[i])) + for j := 0; j < len(grid[i]); j++ { + distancesToPoint[i][j] = -1 + } + } + + start, end := findStartAndEnd(contents) + distancesToPoint[start[0]][start[1]] = 0 + bfsDetermineDistances(grid, start, end, &distancesToPoint) + + count := 0 + + picoSecondsToSave := 100 + + visited := make(map[[2][2]int]bool, 0) // start and end + + for i := 0; i < len(grid); i++ { + for j := 0; j < len(grid[i]); j++ { + for radius := 2; radius < 21; radius++ { + for di := 0; di < radius+1; di++ { + dj := radius - di + for _, dir := range [4][2]int{{i + di, j + dj}, {i + di, j - dj}, {i - di, j + dj}, {i - di, j - dj}} { + newI := dir[0] + newJ := dir[1] + if _, ok := visited[[2][2]int{{i, j}, {newI, newJ}}]; ok { + continue + } + + visited[[2][2]int{{i, j}, {newI, newJ}}] = true + + // Out of bounds checks + if newI < 0 || newJ < 0 || newI > len(grid)-1 || newJ > len(grid[0])-1 { + continue + } + + if grid[newI][newJ] == '#' { + continue + } + + if distancesToPoint[i][j]-distancesToPoint[newI][newJ] >= picoSecondsToSave+radius { + count++ + } + } + } + } + + } + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 20, + Part: 2, + Value: count, + }) +} + +var directions = [4][2]int{ + {1, 0}, // Down + {0, 1}, // Right + {-1, 0}, // Up + {0, -1}, // Left +} + +type queueStruct struct { + pos [2]int + steps int +} + +func bfsDetermineDistances(grid [][]byte, start [2]int, end [2]int, distancesToPoint *[][]int) int { + queue := make([]queueStruct, 0) + + queue = append(queue, queueStruct{ + pos: start, + steps: 0, + }) + + var element queueStruct + + for { + if len(queue) == 0 { + break + } + // Grab the next element in queue + element, queue = queue[0], queue[1:] + + for _, dir := range directions { + newI := element.pos[0] + dir[0] + newJ := element.pos[1] + dir[1] + + // Out of bounds checks + if newI < 0 || newJ < 0 || newI > len(grid)-1 || newJ > len(grid[0])-1 { + continue + } + + if grid[newI][newJ] == '#' { + continue + } + + if (*distancesToPoint)[newI][newJ] != -1 { + continue + } + + (*distancesToPoint)[newI][newJ] = (*distancesToPoint)[element.pos[0]][element.pos[1]] + 1 + + queue = append(queue, queueStruct{ + pos: [2]int{newI, newJ}, + steps: element.steps + 1, + }) + } + } + + return -1 +} + +func findStartAndEnd(contents []string) ([2]int, [2]int) { + var start, end [2]int + foundCount := 0 + for i, line := range contents { + for j, char := range line { + if char == 'S' { + start = [2]int{i, j} + foundCount++ + } + if char == 'E' { + end = [2]int{i, j} + foundCount++ + } + } + if foundCount == 2 { + break + } + } + + return start, end +} + +func parseToGrid(contents []string) [][]byte { + grid := make([][]byte, len(contents)) + for i := 0; i < len(contents); i++ { + grid[i] = make([]byte, len(contents[i])) + for j := 0; j < len(contents[i]); j++ { + grid[i][j] = contents[i][j] + } + } + + return grid +} + +func displayGrid(grid [][]byte) { + for _, row := range grid { + for _, char := range row { + fmt.Print(string(char)) + } + fmt.Println() + } +} + +func cloneGrid(grid [][]byte) [][]byte { + newGrid := make([][]byte, len(grid)) + for i := range grid { + newGrid[i] = make([]byte, len(grid[i])) + copy(newGrid[i], grid[i]) + } + return newGrid +} diff --git a/2024/go/day21/example-input b/2024/go/day21/example-input new file mode 100644 index 0000000..dd73dfd --- /dev/null +++ b/2024/go/day21/example-input @@ -0,0 +1,5 @@ +029A +980A +179A +456A +379A \ No newline at end of file diff --git a/2024/go/day21/main.go b/2024/go/day21/main.go new file mode 100644 index 0000000..dd7d075 --- /dev/null +++ b/2024/go/day21/main.go @@ -0,0 +1,271 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "fmt" + "math" + "os" + "path/filepath" + "runtime" + "strconv" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +var NUM_KEYPAD = [][3]string{ + {"7", "8", "9"}, + {"4", "5", "6"}, + {"1", "2", "3"}, + {"N", "0", "A"}, +} + +var DIR_KEYPAD = [][3]string{ + {"N", "^", "A"}, + {"<", "v", ">"}, +} + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 21, + Part: 1, + Value: computeTotal(contents, 2), + }) +} + +func partTwo(contents []string) { + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 21, + Part: 2, + Value: computeTotal(contents, 25), + }) +} + +func computeTotal(contents []string, numRobots int) int { + dirSequences := computeSequences(DIR_KEYPAD) + numSequences := computeSequences(NUM_KEYPAD) + + // For the top-level, we pre-compute te lengths here as we'll use them later when we look at depths + dirLengths := make(map[[2]string]int) + for key, value := range dirSequences { + dirLengths[key] = len(value[0]) + } + + cacheMap := make(map[string]int) + total := 0 + + for _, line := range contents { + inputs := solve(line, &numSequences) + minLength := math.MaxInt + for _, input := range inputs { + length := computeLength(input, numRobots, &dirSequences, &dirLengths, &cacheMap) + if length < minLength { + minLength = length + } + } + num, _ := strconv.Atoi(line[:len(line)-1]) + total += minLength * num + } + + return total +} + +func solve(input string, sequences *map[[2]string][]string) []string { + options := [][]string{} + for i := 0; i < len(input); i++ { + startChar := "A" + if i > 0 { + startChar = string(input[i-1]) + } + endChar := string(input[i]) + options = append(options, (*sequences)[[2]string{startChar, endChar}]) + } + + var combine func([][]string) []string + combine = func(arrays [][]string) []string { + if len(arrays) == 0 { + return []string{""} + } + first, rest := arrays[0], combine(arrays[1:]) + result := []string{} + for _, f := range first { + for _, r := range rest { + result = append(result, f+r) + } + } + return result + } + + return combine(options) +} + +// Length of certain sequence between 2 points, at a certain depth +func computeLength(seq string, depth int, dirSeqs *map[[2]string][]string, dirLengths *map[[2]string]int, cacheMap *map[string]int) int { + if depth == 1 { + length := 0 + for i := 0; i < len(seq); i++ { + x := "A" + if i > 0 { + x = string(seq[i-1]) + } + y := string(seq[i]) + length += (*dirLengths)[[2]string{x, y}] + } + return length + } + + key := fmt.Sprintf("%s_%d", seq, depth) + if val, exists := (*cacheMap)[key]; exists { + return val + } + + length := 0 + for i := 0; i < len(seq); i++ { + x := "A" + if i > 0 { + x = string(seq[i-1]) + } + y := string(seq[i]) + + minLength := math.MaxInt + for _, subSequence := range (*dirSeqs)[[2]string{x, y}] { + subSequenceLength := computeLength(subSequence, depth-1, dirSeqs, dirLengths, cacheMap) + if subSequenceLength < minLength { + minLength = subSequenceLength + } + } + length += minLength + } + + (*cacheMap)[key] = length + return length +} + +// Build map of getting from A to B for every A,B on the keypad; +func computeSequences(keypad [][3]string) map[[2]string][]string { + // First convert to i,j coords (row,col) + keypadToCoord := make(map[string][2]int, 0) + for i := 0; i < len(keypad); i++ { + for j := 0; j < len(keypad[j]); j++ { + // Can't ever be none, so skip these! + if keypad[i][j] == "N" { + continue + } + keypadToCoord[keypad[i][j]] = [2]int{i, j} + } + } + + // Now let's build the sequences for getting from A to B, could b multiple. Use BFS + return getAllSequencesFromAtoB(keypad, keypadToCoord) +} + +type queueStruct struct { + pos [2]int + currentSequence string +} + +var DIRECTIONS = [4][2]int{ + {-1, 0}, + {0, 1}, + {1, 0}, + {0, -1}, +} + +var DIRECTIONS_BY_ARROW = map[int]string{ + 0: "^", + 1: ">", + 2: "v", + 3: "<", +} + +func getAllSequencesFromAtoB(keypad [][3]string, keypadToCoord map[string][2]int) map[[2]string][]string { + sequences := make(map[[2]string][]string, 0) + + for charStart := range keypadToCoord { + for charTarget := range keypadToCoord { + if charStart == charTarget { + sequences[[2]string{charStart, charTarget}] = []string{"A"} + continue + } + + // BFS; + sequences[[2]string{charStart, charTarget}] = bfs(keypad, charStart, charTarget, keypadToCoord) + + } + + } + + return sequences +} + +// Get the possibilities of all paths from 1 char to the other +func bfs(keypad [][3]string, charStart string, charTarget string, keypadToCoords map[string][2]int) []string { + queue := make([]queueStruct, 0) + queue = append(queue, queueStruct{keypadToCoords[charStart], ""}) + var element queueStruct + possibilities := make([]string, 0) + optimalLength := math.MaxInt64 + + for { + if len(queue) == 0 { + break + } + + // Grab the next element in queue + element, queue = queue[0], queue[1:] + + for dirIndex, dir := range DIRECTIONS { + newI := element.pos[0] + dir[0] + newJ := element.pos[1] + dir[1] + + // Range check; + if newI < 0 || newJ < 0 || newI > len(keypad)-1 || newJ > len(keypad[0])-1 { + continue + } + + // Can't be Null; + if keypad[newI][newJ] == "N" { + continue + } + + if keypad[newI][newJ] == charTarget { + if len(element.currentSequence) > optimalLength { + return possibilities + } + + // Otherwise we'rw at the optimal! + optimalLength = len(element.currentSequence) + 1 + newString := fmt.Sprintf("%s%s%s", element.currentSequence, string(DIRECTIONS_BY_ARROW[dirIndex]), "A") + possibilities = append(possibilities, newString) + continue + } + + // Otherwise, append to the queue + queue = append(queue, queueStruct{[2]int{newI, newJ}, element.currentSequence + string(DIRECTIONS_BY_ARROW[dirIndex])}) + } + } + + return possibilities +} diff --git a/2024/go/day22/example-input b/2024/go/day22/example-input new file mode 100644 index 0000000..cfb1988 --- /dev/null +++ b/2024/go/day22/example-input @@ -0,0 +1,4 @@ +1 +2 +3 +2024 \ No newline at end of file diff --git a/2024/go/day22/main.go b/2024/go/day22/main.go new file mode 100644 index 0000000..85da5a4 --- /dev/null +++ b/2024/go/day22/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "math" + "os" + "path/filepath" + "runtime" + "strconv" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + buyerSecrets := parseInput(contents) + + // Cache the evolutions... could this get a bit too big? + cachedEvolutions := make(map[int64]int64, 0) + + for n, secret := range buyerSecrets { + newSecret := secret + for i := 0; i < 2000; i++ { + newSecret = evolve(newSecret, &cachedEvolutions) + } + buyerSecrets[n] = newSecret + } + + sum := int64(0) + for _, sec := range buyerSecrets { + sum += sec + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 22, + Part: 1, + Value: sum, + }) +} + +func partTwo(contents []string) { + buyerSecrets := parseInput(contents) + + // Cache the evolutions... could this get a bit too big? + cachedEvolutions := make(map[int64]int64, 0) + sequencesToBananasPerBuyer := make([]map[[4]int64]int64, len(buyerSecrets)) + + for n, secret := range buyerSecrets { + sequencesToBananasPerBuyer[n] = make(map[[4]int64]int64, 0) + newSecret := secret + allSecrets := make([]int64, 2001) + allSecrets[0] = secret + for i := 1; i < 2001; i++ { + newSecret = evolve(allSecrets[i-1], &cachedEvolutions) + allSecrets[i] = newSecret + if i > 3 { + key := [4]int64{ + (allSecrets[i-3] % 10) - (allSecrets[i-4] % 10), + (allSecrets[i-2] % 10) - (allSecrets[i-3] % 10), + (allSecrets[i-1] % 10) - (allSecrets[i-2] % 10), + (allSecrets[i] % 10) - (allSecrets[i-1] % 10), + } + if _, ok := sequencesToBananasPerBuyer[n][key]; !ok { + // Already been set - we always take lowest so must skip now! + sequencesToBananasPerBuyer[n][key] = (allSecrets[i] % 10) // Last digit is bananas + } + } + } + buyerSecrets[n] = newSecret + } + + // Now go through each option and store the sum of bananas we get. THis will be prettyh verbose in Go... + bananasPerKey := make(map[[4]int64]int64, 0) + for _, bananasBySequence := range sequencesToBananasPerBuyer { + for seq, bananaCount := range bananasBySequence { + bananasPerKey[seq] += bananaCount + } + } + + currentBest := int64(0) + for _, bananas := range bananasPerKey { + if bananas > currentBest { + currentBest = bananas + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 22, + Part: 2, + Value: currentBest, + }) +} + +func evolve(secret int64, cachedEvolutions *map[int64]int64) int64 { + if val, ok := (*cachedEvolutions)[secret]; ok { + return val + } + + originalSecret := secret + + // Otherwise, go through the steps: + + // Step 1: + res := secret * 64 + secret = (res ^ secret) % 16777216 + + // Step 2: + res = int64(math.Floor(float64(secret) / float64(32))) + secret = (res ^ secret) % 16777216 + + // Step 4: + res = secret * 2048 + secret = (res ^ secret) % 16777216 + + (*cachedEvolutions)[originalSecret] = secret + + return secret +} + +func parseInput(contents []string) []int64 { + ints := make([]int64, 0) + for _, line := range contents { + val, err := strconv.ParseInt(line, 10, 64) + if err != nil { + panic(err) + } + ints = append(ints, val) + } + return ints +} diff --git a/2024/go/day23/example-input b/2024/go/day23/example-input new file mode 100644 index 0000000..d8576a8 --- /dev/null +++ b/2024/go/day23/example-input @@ -0,0 +1,32 @@ +kh-tc +qp-kh +de-cg +ka-co +yn-aq +qp-ub +cg-tb +vc-aq +tb-ka +wh-tc +yn-cg +kh-ub +ta-co +de-co +tc-td +tb-wq +wh-td +ta-ka +td-qp +aq-cg +wq-ub +ub-vc +de-ta +wq-aq +wq-vc +wh-yn +ka-de +kh-ta +co-tc +wh-qp +tb-vc +td-yn \ No newline at end of file diff --git a/2024/go/day23/main.go b/2024/go/day23/main.go new file mode 100644 index 0000000..d31679b --- /dev/null +++ b/2024/go/day23/main.go @@ -0,0 +1,241 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "math" + "os" + "path/filepath" + "runtime" + "sort" + "strings" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + _, contents := sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + nodeMaps := buildMap(contents) + var solutions [][]string + + // Go through each and look for 2 levels deep, the same one again/ Recursive solution or BFS probably would work + for targetString := range nodeMaps { + // We need to go 2 levels deep and see if we get the original string again. If we do, that one is a match. + solutions = append(solutions, hasTargetAfterDepth(2, targetString, targetString, []string{}, &nodeMaps)...) + } + + uniqueSolutionsWithT := make(map[string]bool) + + for i := 0; i < len(solutions); i++ { + hasTString := false + + for _, key := range solutions[i] { + if key[0] == 't' { + hasTString = true + break + } + } + if hasTString { + sort.Slice(solutions[i], func(n, m int) bool { + return solutions[i][n] > solutions[i][m] + }) + + uniqueSolutionsWithT[strings.Join(solutions[i], "-")] = true + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 23, + Part: 1, + Value: len(uniqueSolutionsWithT), + }) +} + +func partTwo(contents []string) { + nodeMaps := buildMap(contents) + + searchesByNode := make(map[string][]string, 0) + for node := range nodeMaps { + searchesByNode[node] = search(node, []string{node}, &nodeMaps) + } + + // fmt.Println(searchesByNode) + + // Make them unique by changing each to a map... kinda wish we were using a language with sets now :c + mapsByNode := make(map[string]map[string]bool, 0) + for node, values := range searchesByNode { + for _, val := range values { + if _, ok := mapsByNode[node]; !ok { + mapsByNode[node] = make(map[string]bool, 0) + } + mapsByNode[node][val] = true + } + } + + // Now we un-map them and then sort... probably could do this better but it hopefully will work... + uniqueSearchesByNode := make(map[string][]string, 0) + for node, nodeMap := range mapsByNode { + uniqueSearchesByNode[node] = make([]string, 0) + for newNode := range nodeMap { + uniqueSearchesByNode[node] = append(uniqueSearchesByNode[node], newNode) + } + } + + // Pick out the longest, any will do (I think...) + longestIndex := "" + currentLongestLength := math.MinInt + for i, v := range uniqueSearchesByNode { + if len(v) > currentLongestLength { + longestIndex = i + currentLongestLength = len(v) + } + } + + sort.Slice(uniqueSearchesByNode[longestIndex], func(i, j int) bool { + return uniqueSearchesByNode[longestIndex][i] < uniqueSearchesByNode[longestIndex][j] + }) + + var builder strings.Builder + + for i, node := range uniqueSearchesByNode[longestIndex] { + builder.WriteString(node) + if i < len(uniqueSearchesByNode[longestIndex])-1 { + builder.WriteString(",") + } + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 23, + Part: 2, + Value: builder.String(), + }) +} + +func search(node string, existingConnections []string, nodeMap *map[string][]string) []string { + // For each connection off the node... + for _, neighbour := range (*nodeMap)[node] { + // Skip if neighbour is already in the existing conns: + isInExisting := false + for _, conn := range existingConnections { + if conn == neighbour { + isInExisting = true + break + } + } + + if isInExisting { + continue + } + + // Neighbour must be connected to everything in the set: + connectedToEverything := true + for _, queryNode := range existingConnections { + // For each of these, it must be connected to the new node + if !nodeIsAttached(queryNode, neighbour, nodeMap) { + connectedToEverything = false + break + } + } + + if !connectedToEverything { + continue + } + + // Add this node and then trial all it's neighbours + existingConnections = append(existingConnections, neighbour) + + existingConnections = append(existingConnections, search(neighbour, existingConnections, nodeMap)...) + } + + return existingConnections +} + +func nodeIsAttached(baseNode string, searchNode string, nodeMap *map[string][]string) bool { + for _, neighbour := range (*nodeMap)[baseNode] { + if neighbour == searchNode { + return true + } + } + + return false +} + +func hasTargetAfterDepth(depth int, currentString string, targetString string, previousStrings []string, nodeMap *map[string][]string) [][]string { + targets := make([][]string, 0) + + previousStrings = append(previousStrings, currentString) + + if depth == 0 { + // We need the target string to be inside the map for the current string + for _, possibleTargetString := range (*nodeMap)[currentString] { + if possibleTargetString == targetString { + targets = append(targets, previousStrings) + return targets + } + } + return targets // not a valid path + } + + // Otherwise, call this again and drop the depth by 1 + for _, nextString := range (*nodeMap)[currentString] { + targets = append(targets, hasTargetAfterDepth(depth-1, nextString, targetString, previousStrings, nodeMap)...) + } + + return targets +} + +// func expectOutputAfterDepth(depthRemaining int, currentString string, targetString string, previousStrings []string, nodeMap *map[string][]string) (bool, []string) { +// if depthRemaining == 0 { +// // We need the target string to be inside the map for the current string +// for _, possibleTargetString := range (*nodeMap)[currentString] { +// if possibleTargetString == targetString { +// return true, previousStrings +// } +// } +// return false, []string{} +// } + +// // Otherwise, call this again and drop the depth by 1 +// for _, nextString := range (*nodeMap)[currentString] { + +// } + +// return false, []string{} +// } + +func buildMap(contents []string) map[string][]string { + nodeMap := make(map[string][]string, 0) + for _, line := range contents { + stringsTemp := strings.Split(line, "-") + if _, ok := nodeMap[stringsTemp[0]]; !ok { + nodeMap[stringsTemp[0]] = make([]string, 0) + } + nodeMap[stringsTemp[0]] = append(nodeMap[stringsTemp[0]], stringsTemp[1]) + if _, ok := nodeMap[stringsTemp[1]]; !ok { + nodeMap[stringsTemp[1]] = make([]string, 0) + } + nodeMap[stringsTemp[1]] = append(nodeMap[stringsTemp[1]], stringsTemp[0]) + } + + return nodeMap +} diff --git a/2024/go/day24/example-input b/2024/go/day24/example-input new file mode 100644 index 0000000..09fb230 --- /dev/null +++ b/2024/go/day24/example-input @@ -0,0 +1,47 @@ +x00: 1 +x01: 0 +x02: 1 +x03: 1 +x04: 0 +y00: 1 +y01: 1 +y02: 1 +y03: 1 +y04: 1 + +ntg XOR fgs -> mjb +y02 OR x01 -> tnw +kwq OR kpj -> z05 +x00 OR x03 -> fst +tgd XOR rvg -> z01 +vdt OR tnw -> bfw +bfw AND frj -> z10 +ffh OR nrd -> bqk +y00 AND y03 -> djm +y03 OR y00 -> psh +bqk OR frj -> z08 +tnw OR fst -> frj +gnj AND tgd -> z11 +bfw XOR mjb -> z00 +x03 OR x00 -> vdt +gnj AND wpb -> z02 +x04 AND y00 -> kjc +djm OR pbm -> qhw +nrd AND vdt -> hwm +kjc AND fst -> rvg +y04 OR y02 -> fgs +y01 AND x02 -> pbm +ntg OR kjc -> kwq +psh XOR fgs -> tgd +qhw XOR tgd -> z09 +pbm OR djm -> kpj +x03 XOR y03 -> ffh +x00 XOR y04 -> ntg +bfw OR bqk -> z06 +nrd XOR fgs -> wpb +frj XOR qhw -> z04 +bqk OR frj -> z07 +y03 OR x01 -> nrd +hwm AND bqk -> z03 +tgd XOR rvg -> z12 +tnw OR pbm -> gnj \ No newline at end of file diff --git a/2024/go/day24/main.go b/2024/go/day24/main.go new file mode 100644 index 0000000..f9ff4f6 --- /dev/null +++ b/2024/go/day24/main.go @@ -0,0 +1,172 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "fmt" + "os" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + _, contents := sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + valuesMap, instructions := parseInput(contents) + counter := 0 + + for { + instruction := instructions[counter] + + // If both inputs exist in the map, we can run! + if valuesMap[instruction.inputOne] == -1 || valuesMap[instruction.inputTwo] == -1 { + counter = (counter + 1) % len(instructions) + continue + } + + if instruction.operator == "OR" { + if valuesMap[instruction.inputOne] == 1 || valuesMap[instruction.inputTwo] == 1 { + valuesMap[instruction.outputKey] = 1 + } else { + valuesMap[instruction.outputKey] = 0 + } + } else if instruction.operator == "AND" { + if valuesMap[instruction.inputOne] == 1 && valuesMap[instruction.inputTwo] == 1 { + valuesMap[instruction.outputKey] = 1 + } else { + valuesMap[instruction.outputKey] = 0 + } + } else { + valuesMap[instruction.outputKey] = valuesMap[instruction.inputOne] ^ valuesMap[instruction.inputTwo] + } + + if haveAllProcessed(&valuesMap) { + break + } + + counter = (counter + 1) % len(instructions) + } + + // Step 1: Filter the keys that start with 'z' + zKeys := make([]string, 0) + for key := range valuesMap { + if strings.HasPrefix(key, "z") { + zKeys = append(zKeys, key) + } + } + + sort.Strings(zKeys) // Sort the filtered keys + + var binaryValues strings.Builder + for i := len(zKeys) - 1; i >= 0; i-- { + key := zKeys[i] + newString := strconv.FormatInt(int64(valuesMap[key]), 10) + fmt.Println(key, valuesMap[key], newString) + binaryValues.WriteString(newString) + } + + fmt.Println(binaryValues.String()) + + intValue, err := strconv.ParseInt(binaryValues.String(), 2, 64) + if err != nil { + fmt.Println("Error:", err) + return + } + + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 24, + Part: 1, + Value: intValue, + }) +} + +func haveAllProcessed(valuesMap *map[string]int) bool { + for _, val := range *valuesMap { + if val == -1 { + return false + } + } + return true +} + +func partTwo(contents []string) { + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 24, + Part: 2, + Value: "TODO", + }) +} + +type instructionStruct struct { + inputOne string + operator string + inputTwo string + outputKey string +} + +func parseInput(contents []string) (map[string]int, []instructionStruct) { + valuesMap := make(map[string]int, 0) + instructions := make([]instructionStruct, 0) + for _, line := range contents { + if len(line) == 0 { + continue + } + if line[3] == ':' { + value := line[len(line)-1] + intVal, err := strconv.Atoi(string(value)) + if err != nil { + panic(err) + } + valuesMap[line[0:3]] = intVal + continue + } + + // Else it's an instructions map: + values := strings.Fields(line) + instructions = append(instructions, instructionStruct{ + inputOne: values[0], + operator: values[1], + inputTwo: values[2], + outputKey: values[4], + }) + + // Also add to the values map as -1 for the 3 keys; + if _, ok := valuesMap[values[0]]; !ok { + valuesMap[values[0]] = -1 // Default to -1 + } + + if _, ok := valuesMap[values[2]]; !ok { + valuesMap[values[2]] = -1 // Default to -1 + } + + if _, ok := valuesMap[values[4]]; !ok { + valuesMap[values[4]] = -1 // Default to -1 + } + } + + return valuesMap, instructions +} diff --git a/2024/go/day25/example-input b/2024/go/day25/example-input new file mode 100644 index 0000000..a53fe9b --- /dev/null +++ b/2024/go/day25/example-input @@ -0,0 +1,39 @@ +##### +.#### +.#### +.#### +.#.#. +.#... +..... + +##### +##.## +.#.## +...## +...#. +...#. +..... + +..... +#.... +#.... +#...# +#.#.# +#.### +##### + +..... +..... +#.#.. +###.. +###.# +###.# +##### + +..... +..... +..... +#.... +#.#.. +#.#.# +##### \ No newline at end of file diff --git a/2024/go/day25/main.go b/2024/go/day25/main.go new file mode 100644 index 0000000..39eb262 --- /dev/null +++ b/2024/go/day25/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var _, contents = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents []string) { + locks, keys := parseInput(contents) + + uniquePairs := 0 + for _, key := range keys { + // Try all locks; + for _, lock := range locks { + // Does the ey fit in the lock? + if fitInLock(key, lock) { + uniquePairs++ + } + } + + } + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 25, + Part: 1, + Value: uniquePairs, + }) +} + +func fitInLock(key [5]int, lock [5]int) bool { + for i := 0; i < 5; i++ { + // Overlap if the sum is bigger than 5 + if key[i]+lock[i] > 5 { + return false + } + } + + return true +} +func partTwo(contents []string) { + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 25, + Part: 2, + Value: "Merry Christmas", + }) +} + +func parseInput(contents []string) ([][5]int, [][5]int) { + locks := make([][5]int, 0) + keys := make([][5]int, 0) + for i := 0; i < len(contents); i += 8 { + current := [5]int{} + if contents[i] == "#####" { + for j := 1; j < 8; j++ { + for n := range contents[j] { + if contents[i+j][n] == '.' && contents[i+j-1][n] == '#' { + current[n] = j - 1 + } + } + } + locks = append(locks, current) + } else { + for j := 6; j >= 0; j-- { + for n := range contents[j] { + if contents[i+j][n] == '#' && contents[i+j-1][n] == '.' { + current[n] = 6 - j + } + } + } + keys = append(keys, current) + } + } + + return locks, keys +} diff --git a/2024/go/dayx/example-input b/2024/go/dayx/example-input new file mode 100644 index 0000000..4651466 --- /dev/null +++ b/2024/go/dayx/example-input @@ -0,0 +1,5 @@ +H => HO +H => OH +O => HH + +HOH \ No newline at end of file diff --git a/2024/go/dayx/main.go.stub b/2024/go/dayx/main.go.stub new file mode 100644 index 0000000..6b51804 --- /dev/null +++ b/2024/go/dayx/main.go.stub @@ -0,0 +1,48 @@ +package main + +import ( + "aoc-shared/pkg/sharedcode" + "aoc-shared/pkg/sharedstruct" + "os" + "path/filepath" + "runtime" +) + +func getCurrentDirectory() string { + _, filename, _, _ := runtime.Caller(0) + dirname := filepath.Dir(filename) + return dirname +} + +// Default Input path is current directory + example-input +var inputPath = filepath.Join(getCurrentDirectory(), "example-input") +var isUsingExample = true + +func main() { + // If another cmd argument has been passed, use that as the input path: + if len(os.Args) > 1 { + inputPath = os.Args[1] + isUsingExample = false + } + + var contents, _ = sharedcode.ParseFile(inputPath) + + partOne(contents) + partTwo(contents) +} + +func partOne(contents string) { + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 0, + Part: 1, + Value: "TODO", + }) +} + +func partTwo(contents string) { + sharedstruct.PrintOutput(sharedstruct.Output{ + Day: 0, + Part: 2, + Value: "TODO", + }) +} diff --git a/2024/go/go.mod b/2024/go/go.mod new file mode 100644 index 0000000..04cfcd3 --- /dev/null +++ b/2024/go/go.mod @@ -0,0 +1,3 @@ +module aoc-2024 + +go 1.23.4 diff --git a/2024/ts/day01/example-input b/2024/ts/day01/example-input new file mode 100644 index 0000000..dfca0b1 --- /dev/null +++ b/2024/ts/day01/example-input @@ -0,0 +1,6 @@ +3 4 +4 3 +2 5 +1 3 +3 9 +3 3 \ No newline at end of file diff --git a/2024/ts/day01/main.ts b/2024/ts/day01/main.ts new file mode 100644 index 0000000..ca20e4d --- /dev/null +++ b/2024/ts/day01/main.ts @@ -0,0 +1,60 @@ +import { fromFileUrl, dirname } from "jsr:@std/path"; + +// --- SETUP --- +const text = await readFile(); +const [leftList, rightList] = parseInput(text); + +leftList.sort((a, b) => a - b); +rightList.sort((a, b) => a - b); + +// --- PART 1 --- +let difference = 0; + +for (let i = 0; i < leftList.length; i++) { + difference += Math.abs(leftList[i] - rightList[i]); +} + +console.log({ difference }); + +// --- PART 2 --- +let similarityScore = 0; + +leftList.forEach((val) => { + let qty = 0; + + for (let j = 0; j < rightList.length; j++) { + if (val === rightList[j]) { + qty++; + } + if (rightList[j] > val) { + break; + } + } + + similarityScore += qty * val; +}); + +console.log({ similarityScore }); + +async function readFile(): string { + let path = dirname(fromFileUrl(import.meta.url)) + "/example-input"; + if (Deno.args.length > 0) { + // If we pass an argument, use this as the input + path = Deno.args[0]; + } + + return await Deno.readTextFile(path); +} + +function parseInput(input: string): [number[], number[]] { + let leftList = [] as number[]; + let rightList = [] as number[]; + input.split("\n").forEach((line) => { + let [left, right] = line.split(" ").map(Number); + + leftList.push(left); + rightList.push(right); + }); + + return [leftList, rightList]; +} diff --git a/README.md b/README.md index a4d37f3..79ea853 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ I started with 2023, and am now working my way from 2015. Keep an eye on the lis - [ ] 2022 - [x] 2023 - JavaScript +- [ ] 2024 + - Go (WIP) ## Go @@ -32,6 +34,7 @@ to run with example input, or ```bash go run ./2015/go/day01 ./input ``` + to run with the input at the path given. ## Templates @@ -39,15 +42,18 @@ to run with the input at the path given. ### JavaScript You can run + ```bash bash createJsDay.sh ``` + to create the template for a new day in JavaScript. To do this, ensure that there is a `dayx` template folder in the destination. _Tip: Add '0' to the start of the day number if it is less than 10, e.g. `01` instead of `1`, for better ordering!_ ### Go Run + ```bash bash createGoDay.sh ``` diff --git a/go.work b/go.work index 97ee78f..2de2134 100644 --- a/go.work +++ b/go.work @@ -1,5 +1,6 @@ -go 1.21.5 +go 1.23.4 use ./shared/go use ./2015/go -use ./2016/go \ No newline at end of file +use ./2016/go +use ./2024/go \ No newline at end of file diff --git a/shared/go/go.mod b/shared/go/go.mod index 3776dbe..2c4de92 100644 --- a/shared/go/go.mod +++ b/shared/go/go.mod @@ -1,3 +1,3 @@ module aoc-shared -go 1.21.5 +go 1.23.4