diff --git a/2022/day05/main.go b/2022/day05/main.go new file mode 100644 index 0000000..a56840a --- /dev/null +++ b/2022/day05/main.go @@ -0,0 +1,126 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) string { + stacks, steps := parseInput(input) + + for _, step := range steps { + // move crates ONE AT A TIME + for q := 0; q < step.qty; q++ { + top := stacks[step.from][len(stacks[step.from])-1] + stacks[step.to] = append(stacks[step.to], top) + stacks[step.from] = stacks[step.from][:len(stacks[step.from])-1] + } + } + + ans := "" + for _, stack := range stacks { + ans += stack[len(stack)-1] + } + return ans +} + +func part2(input string) string { + stacks, steps := parseInput(input) + + for _, step := range steps { + // move crates ONCE + fromIndex := len(stacks[step.from]) - step.qty + stacks[step.to] = append(stacks[step.to], stacks[step.from][fromIndex:]...) + stacks[step.from] = stacks[step.from][:fromIndex] + } + + ans := "" + for _, stack := range stacks { + ans += stack[len(stack)-1] + } + return ans +} + +// move 4 from 3 to 1 +type step struct { + qty, from, to int +} + +func (s step) String() string { + return fmt.Sprintf("move %d from %d to %d", s.qty, s.from, s.to) +} + +func parseInput(input string) ([][]string, []step) { + parts := strings.Split(input, "\n\n") + + state := parts[0] + oversized := [][]string{} + for _, row := range strings.Split(state, "\n") { + oversized = append(oversized, strings.Split(row, "")) + } + oRows, oCols := len(oversized), len(oversized[0]) + + actual := [][]string{} + + for c := 0; c < oCols-1; c++ { + if oversized[oRows-1][c] != " " { + // hit a column with values... move up from here + stack := []string{} + for r := oRows - 2; r >= 0; r-- { + char := oversized[r][c] + if char != " " { + stack = append(stack, char) + } + } + actual = append(actual, stack) + } + } + + stepsRaw := parts[1] + steps := []step{} + for _, row := range strings.Split(stepsRaw, "\n") { + inst := step{} + _, err := fmt.Sscanf(row, "move %d from %d to %d", &inst.qty, &inst.from, &inst.to) + if err != nil { + panic(err) + } + // subtract one so they're zero indexed... + inst.from-- + inst.to-- + steps = append(steps, inst) + } + + return actual, steps +} diff --git a/2022/day05/main_test.go b/2022/day05/main_test.go new file mode 100644 index 0000000..d0cfba1 --- /dev/null +++ b/2022/day05/main_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "testing" +) + +var example = ` [D] +[N] [C] +[Z] [M] [P] + 1 2 3 + +move 1 from 2 to 1 +move 3 from 1 to 3 +move 2 from 2 to 1 +move 1 from 1 to 2` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "example", + input: example, + want: "CMZ", + }, + { + name: "actual", + input: input, + want: "QNHWJVJZW", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "example", + input: example, + want: "MCD", + }, + { + name: "actual", + input: input, + want: "BPCZJLFJW", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day06/main.go b/2022/day06/main.go new file mode 100644 index 0000000..e7362bc --- /dev/null +++ b/2022/day06/main.go @@ -0,0 +1,75 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + // packet starts w/ 4 characters that are all different + for i := 0; i+4 <= len(input); i++ { + if allDifferentLetters(input[i : i+4]) { + return i + 4 + } + } + + return -1 +} + +// lazy but easier than sliding window... +func allDifferentLetters(str string) bool { + // if len(str) != 4 { + // panic(fmt.Sprintf("invalid length %q", str)) + // } + for i := 0; i < len(str); i++ { + for j := i + 1; j < len(str); j++ { + if str[i] == str[j] { + return false + } + } + } + return true +} + +func part2(input string) int { + // wow super lazy but fast to write... ok + for i := 0; i+14 <= len(input); i++ { + if allDifferentLetters(input[i : i+14]) { + return i + 14 + } + } + + return -1 +} diff --git a/2022/day06/main_test.go b/2022/day06/main_test.go new file mode 100644 index 0000000..6babf10 --- /dev/null +++ b/2022/day06/main_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "testing" +) + +var example = `mjqjpqmgbljsphdztnvjfqwrcgsmlb` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 7, + }, + { + name: "actual", + input: input, + want: 1109, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 19, + }, + { + name: "actual", + input: input, + want: 3965, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day07/main.go b/2022/day07/main.go new file mode 100644 index 0000000..2f8ef36 --- /dev/null +++ b/2022/day07/main.go @@ -0,0 +1,168 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "math" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + root := parseInput(input) + + return sumDirsUnder100000(root) +} + +func sumDirsUnder100000(itr *dir) int { + SizeLimit := 100000 + + sum := 0 + if itr.totalSize <= SizeLimit { + sum += itr.totalSize + } + for _, child := range itr.childDirs { + sum += sumDirsUnder100000(child) + } + return sum +} + +func part2(input string) int { + root := parseInput(input) + totalSapceAvailable := 70000000 + spaceNeeded := 30000000 + + // find smallest directory to be deleted that would free up enough space... + directoryMinSize := spaceNeeded - (totalSapceAvailable - root.totalSize) + return findSmallestDirToDelete(root, directoryMinSize) +} + +func findSmallestDirToDelete(itr *dir, directoryMinSize int) int { + smallest := math.MaxInt64 + if itr.totalSize >= directoryMinSize { + smallest = mathy.MinInt(smallest, itr.totalSize) + } + + for _, childDirs := range itr.childDirs { + smallest = mathy.MinInt(smallest, findSmallestDirToDelete(childDirs, directoryMinSize)) + } + + return smallest +} + +type dir struct { + name string + parentDir *dir + childDirs map[string]*dir + files map[string]int + totalSize int +} + +func parseInput(input string) *dir { + root := &dir{ + name: "root", + childDirs: map[string]*dir{}, + } + itr := root + + cmds := strings.Split(input, "\n") + c := 0 + + for c < len(cmds) { + switch cmd := cmds[c]; cmd[0:1] { + case "$": + if cmd == "$ ls" { + // just move on, we will assume we're always in an listing state + c++ + } else { + changeDir := strings.Split(cmd, "cd ")[1] + changeDir = strings.TrimSpace(changeDir) + if changeDir == ".." { + itr = itr.parentDir + } else { + // if changeDir doesn't exist.. + if _, ok := itr.childDirs[changeDir]; !ok { + itr.childDirs[changeDir] = &dir{ + name: changeDir, + parentDir: itr, + childDirs: map[string]*dir{}, + files: map[string]int{}} + } + + itr = itr.childDirs[changeDir] + } + c++ + } + default: + // assume we're listing a dir's contents... add it + if strings.HasPrefix(cmd, "dir") { + childDirName := cmd[4:] + if _, ok := itr.childDirs[childDirName]; !ok { + itr.childDirs[childDirName] = &dir{ + name: childDirName, + parentDir: itr, + childDirs: map[string]*dir{}, + files: map[string]int{}, + } + } + } else { + // file name + parts := strings.Split(cmd, " ") + itr.files[parts[0]] = cast.ToInt(parts[0]) + } + c++ + } + } + + populateFileSizes(root) + return root +} + +func populateFileSizes(itr *dir) int { + totalSize := 0 + + for _, childItr := range itr.childDirs { + totalSize += populateFileSizes(childItr) + } + + for _, sz := range itr.files { + totalSize += sz + } + + itr.totalSize = totalSize + + return totalSize + +} diff --git a/2022/day07/main_test.go b/2022/day07/main_test.go new file mode 100644 index 0000000..1566311 --- /dev/null +++ b/2022/day07/main_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "testing" +) + +var example = `$ cd / +$ ls +dir a +14848514 b.txt +8504156 c.dat +dir d +$ cd a +$ ls +dir e +29116 f +2557 g +62596 h.lst +$ cd e +$ ls +584 i +$ cd .. +$ cd .. +$ cd d +$ ls +4060174 j +8033020 d.log +5626152 d.ext +7214296 k` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 95437, + }, + { + name: "actual", + input: input, + want: 1423358, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 24933642, + }, + { + name: "actual", + input: input, + want: 545729, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day08/main.go b/2022/day08/main.go new file mode 100644 index 0000000..5f7f108 --- /dev/null +++ b/2022/day08/main.go @@ -0,0 +1,144 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + grid := parseInput(input) + + // may be visible from multiple angles + visibleCoords := map[[2]int]string{} + for r := 1; r < len(grid)-1; r++ { + // from left + highestFromLeft := -1 + for c := 0; c < len(grid[0])-1; c++ { + height := grid[r][c] + if height > highestFromLeft { + visibleCoords[[2]int{r, c}] = "L" + highestFromLeft = height + } + } + // from right + highestFromRight := -1 + for c := len(grid[0]) - 1; c > 0; c-- { + height := grid[r][c] + if height > highestFromRight { + visibleCoords[[2]int{r, c}] = "R" + highestFromRight = height + } + } + } + + for c := 1; c < len(grid[0])-1; c++ { + // from top + highestFromTop := -1 + for r := 0; r < len(grid)-1; r++ { + height := grid[r][c] + if height > highestFromTop { + visibleCoords[[2]int{r, c}] = "T" + highestFromTop = height + } + } + // from bottom + highestFromBottom := -1 + for r := len(grid) - 1; r > 0; r-- { + height := grid[r][c] + if height > highestFromBottom { + visibleCoords[[2]int{r, c}] = "B" + highestFromBottom = height + } + } + } + + return len(visibleCoords) + 4 // plus 4 for corners +} + +func part2(input string) int { + // multiply the four scores together... score = how many trees any tree can see + // because trees on the edge will have a zero, just ignore them + grid := parseInput(input) + + bestScore := 0 + // iterate through every eligible tree + for r := 1; r < len(grid)-1; r++ { + for c := 1; c < len(grid[0])-1; c++ { + score := visible(grid, r, c, -1, 0) + score *= visible(grid, r, c, 1, 0) + score *= visible(grid, r, c, 0, -1) + score *= visible(grid, r, c, 0, 1) + + if score > bestScore { + bestScore = score + } + } + } + + return bestScore +} + +func visible(grid [][]int, r, c, dr, dc int) int { + count := 0 + startingHeight := grid[r][c] + r += dr + c += dc + for r >= 0 && r < len(grid) && c >= 0 && c < len(grid[0]) { + height := grid[r][c] + if height < startingHeight { + count++ + } else { + count++ + break + } + + r += dr + c += dc + } + + return count +} + +func parseInput(input string) (ans [][]int) { + for _, line := range strings.Split(input, "\n") { + var row []int + for _, n := range strings.Split(line, "") { + row = append(row, cast.ToInt(n)) + } + ans = append(ans, row) + } + return ans +} diff --git a/2022/day08/main_test.go b/2022/day08/main_test.go new file mode 100644 index 0000000..bb6eda5 --- /dev/null +++ b/2022/day08/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "testing" +) + +var example = `30373 +25512 +65332 +33549 +35390` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 21, + }, + { + name: "actual", + input: input, + want: 1690, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 8, + }, + { + name: "actual", + input: input, + want: 535680, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day09/main.go b/2022/day09/main.go new file mode 100644 index 0000000..0e24701 --- /dev/null +++ b/2022/day09/main.go @@ -0,0 +1,253 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + // tail follows head logically if in the same row or column + // if not in same row or column, always moves diagonally + insts := parseInput(input) + + // start stacked at 0,0 + var head, tail [2]int + + // "normal" grid mapping... + diffs := map[string][2]int{ + "U": {1, 0}, + "D": {-1, 0}, + "L": {0, -1}, + "R": {0, 1}, + } + + visited := map[[2]int]bool{ + {0, 0}: true, + } + for _, inst := range insts { + for inst.val > 0 { + // move head + diff := diffs[inst.dir] + head[0] += diff[0] // row + head[1] += diff[1] // col + + // update tail + // if diff to row or col is > 1 + + rowDiff := head[0] - tail[0] + colDiff := head[1] - tail[1] + + // if either row or col diff is > 1, then that dimension HAS to move + // additionally, if the other diff is not zero, it needs to be + // adjusted to move diagonally + // note: the nested if blocks screwed me in part 2 because a longer + // rope can make coordinates off by 2 rows AND 2 cols + if mathy.AbsInt(rowDiff) > 1 { + /* 0 1 2 + H . T + diff = head - tail = -2 + want to make tail (2) to (1), so add diff / 2 + + T . H + diff = 2 - 0 = 2 + tail (0) + 2/2 = 1, checks out still + */ + tail[0] += rowDiff / 2 + // account for diagonal adjustment, same math... add col diff + if colDiff != 0 { + tail[1] += colDiff + } + } else if mathy.AbsInt(colDiff) > 1 { + tail[1] += colDiff / 2 + // account for diagonal adjustment, same math... add col diff + if rowDiff != 0 { + tail[0] += rowDiff + } + } + + // update where the tail has been... + visited[tail] = true + inst.val-- // one step at a time + } + } + + // return spots TAIL visited at least once, map[[2]int]bool + return len(visited) +} + +type inst struct { + dir string + val int +} + +func parseInput(input string) (ans []inst) { + for _, line := range strings.Split(input, "\n") { + ans = append(ans, inst{ + dir: line[:1], + val: cast.ToInt(line[2:]), + }) + } + return ans +} + +func part2(input string) int { + // oof, quite the refactor... + insts := parseInput(input) + + rope := initRope(10) + + visited := map[[2]int]bool{} + for _, inst := range insts { + for inst.val > 0 { + rope.moveOneSpace(inst.dir) + + // update where the tail has been... + visited[rope.tail.coords] = true + + inst.val-- // one step at a time + + fmt.Println(inst, rope, len(visited)) + } + } + + return len(visited) +} + +type node struct { + coords [2]int // row, col still + next *node +} + +type rope struct { + head, tail *node +} + +func initRope(length int) rope { + head := &node{} + itr := head + + // start at 1 to account for head already being created + for i := 1; i < length; i++ { + itr.next = &node{} + itr = itr.next + } + + return rope{ + head: head, + tail: itr, + } +} + +func (r rope) moveOneSpace(dir string) { + // "normal" grid mapping... + diffs := map[string][2]int{ + "U": {1, 0}, + "D": {-1, 0}, + "L": {0, -1}, + "R": {0, 1}, + } + + diff := diffs[dir] + r.head.coords[0] += diff[0] + r.head.coords[1] += diff[1] + + // update rest of rope too + r.head.updateTrailer() +} + +func (r rope) String() string { + str := "" + i := 0 + for itr := r.head; itr != nil; itr = itr.next { + str += fmt.Sprintf("%d:[%d,%d]->", i, itr.coords[0], itr.coords[1]) + i++ + } + return str +} + +// recursively updates the node behind itself as it follows +func (n *node) updateTrailer() { + if n.next == nil { + return + } + + rowDiff := n.coords[0] - n.next.coords[0] + colDiff := n.coords[1] - n.next.coords[1] + + // if either row or col diff is > 1, then that dimension HAS to move + // additionally, if the other diff is not zero, it needs to be + // adjusted to move diagonally + if mathy.AbsInt(rowDiff) > 1 && mathy.AbsInt(colDiff) > 1 { + n.next.coords[0] += rowDiff / 2 + n.next.coords[1] += colDiff / 2 + } else if mathy.AbsInt(rowDiff) > 1 { + // see part1 for math logic + n.next.coords[0] += rowDiff / 2 + n.next.coords[1] += colDiff + } else if mathy.AbsInt(colDiff) > 1 { + n.next.coords[1] += colDiff / 2 + n.next.coords[0] += rowDiff + } else { + // no need to continue updating children if movement is over + return + } + + // go to next node + n.next.updateTrailer() +} + +func reImplPart1(input string) int { + // oof, quite the refactor... + insts := parseInput(input) + + rope := initRope(2) + + visited := map[[2]int]bool{} + for _, inst := range insts { + for inst.val > 0 { + rope.moveOneSpace(inst.dir) + + // update where the tail has been... + visited[rope.tail.coords] = true + + inst.val-- // one step at a time + } + } + + // return spots TAIL visited at least once, map[[2]int]bool + return len(visited) +} diff --git a/2022/day09/main_test.go b/2022/day09/main_test.go new file mode 100644 index 0000000..cadaffc --- /dev/null +++ b/2022/day09/main_test.go @@ -0,0 +1,85 @@ +package main + +import ( + "testing" +) + +var example = `R 4 +U 4 +L 3 +D 1 +R 4 +D 1 +L 5 +R 2` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 13, + }, + { + name: "actual", + input: input, + want: 6236, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + t.Run("reimplementation_"+tt.name, func(t *testing.T) { + if got := reImplPart1(tt.input); got != tt.want { + t.Errorf("reImplPart1() = %v, want %v", got, tt.want) + } + }) + } +} + +var largerExample = `R 5 +U 8 +L 8 +D 3 +R 17 +D 10 +L 25 +U 20` + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 1, + }, + { + name: "larger_example", + input: largerExample, + want: 36, + }, + { + name: "actual", + input: input, + want: 2449, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day10/main.go b/2022/day10/main.go new file mode 100644 index 0000000..5efac70 --- /dev/null +++ b/2022/day10/main.go @@ -0,0 +1,167 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + instructions := parseInput(input) + + X := 1 + sum := 0 + + i := 0 // what the current instruction is + for cycle := 1; cycle <= 220; cycle++ { + // "during" equates to the start of the cycle... + if (cycle-20)%40 == 0 { + sum += X * cycle + } + + switch instructions[i].name { + case "addx": + // decrement cycles on that instruction + // IF it hits zero add V + // AND move to next step + instructions[i].cycles-- + if instructions[i].cycles == 0 { + X += instructions[i].val + i++ + } + case "noop": + // just increment to next instruction + i++ + } + } + + return sum +} + +func part2(input string) string { + instructions := parseInput(input) + + X := 1 // doubles as sprite's center coordinate + + // 6 rows by 40 wide screen, starts all off + CRT := [6][40]string{} + for i, rows := range CRT { + for j := range rows { + CRT[i][j] = "." + } + } + + i := 0 // what the current instruction is + for cycle := 1; i < len(instructions); cycle++ { + // if (cycle-20)%40 == 0 { + // sum += X * cycle + // } + + /* + X = horizontal position of middle of (3 pixel wide) sprite + axis draws left to right, top to bottom, 40 wide x 6 high + 1---40 + 41---80 + ... + 201---240 + + draws 1 pixel per cycle + light up pixels IF the pixel being drawn is the same as one of the sprite's 3 pixels + + */ + + // calculate which pixel is being drawn... ZERO INDEXED + pixelRow := (cycle - 1) / 40 + pixelCol := (cycle - 1) % 40 + + // see if the spite's horizontal location overlaps that pixelCol + spriteLeft, spriteRight := X-1, X+1 + if spriteLeft <= pixelCol && spriteRight >= pixelCol { + CRT[pixelRow][pixelCol] = "#" + } + + switch instructions[i].name { + case "addx": + // decrement cycles on that instruction + // IF it hits zero add V + // AND move to next step + instructions[i].cycles-- + if instructions[i].cycles == 0 { + X += instructions[i].val + i++ + } + case "noop": + // just increment to next instruction + i++ + } + + } + log := "" + for _, rows := range CRT { + for _, cell := range rows { + log += cell + } + log += "\n" + } + fmt.Println(log) + return log +} + +type instruction struct { + name string + val int + cycles int +} + +func parseInput(input string) (ans []instruction) { + for _, l := range strings.Split(input, "\n") { + switch l[:4] { + case "addx": + ans = append(ans, instruction{ + name: "addx", + val: cast.ToInt(l[5:]), + cycles: 2, + }) + case "noop": + ans = append(ans, instruction{ + name: "noop", + cycles: 1, + }) + default: + panic("input line: " + l) + } + } + return ans +} diff --git a/2022/day10/main_test.go b/2022/day10/main_test.go new file mode 100644 index 0000000..e891291 --- /dev/null +++ b/2022/day10/main_test.go @@ -0,0 +1,216 @@ +package main + +import ( + "testing" +) + +var example = `addx 15 +addx -11 +addx 6 +addx -3 +addx 5 +addx -1 +addx -8 +addx 13 +addx 4 +noop +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx -35 +addx 1 +addx 24 +addx -19 +addx 1 +addx 16 +addx -11 +noop +noop +addx 21 +addx -15 +noop +noop +addx -3 +addx 9 +addx 1 +addx -3 +addx 8 +addx 1 +addx 5 +noop +noop +noop +noop +noop +addx -36 +noop +addx 1 +addx 7 +noop +noop +noop +addx 2 +addx 6 +noop +noop +noop +noop +noop +addx 1 +noop +noop +addx 7 +addx 1 +noop +addx -13 +addx 13 +addx 7 +noop +addx 1 +addx -33 +noop +noop +noop +addx 2 +noop +noop +noop +addx 8 +noop +addx -1 +addx 2 +addx 1 +noop +addx 17 +addx -9 +addx 1 +addx 1 +addx -3 +addx 11 +noop +noop +addx 1 +noop +addx 1 +noop +noop +addx -13 +addx -19 +addx 1 +addx 3 +addx 26 +addx -30 +addx 12 +addx -1 +addx 3 +addx 1 +noop +noop +noop +addx -9 +addx 18 +addx 1 +addx 2 +noop +noop +addx 9 +noop +noop +noop +addx -1 +addx 2 +addx -37 +addx 1 +addx 3 +noop +addx 15 +addx -21 +addx 22 +addx -6 +addx 1 +noop +addx 2 +addx 1 +noop +addx -10 +noop +noop +addx 20 +addx 1 +addx 2 +addx 2 +addx -6 +addx -11 +noop +noop +noop` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 13140, + }, + { + name: "actual", + input: input, + want: 15880, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "example", + input: example, + want: `##..##..##..##..##..##..##..##..##..##.. +###...###...###...###...###...###...###. +####....####....####....####....####.... +#####.....#####.....#####.....#####..... +######......######......######......#### +#######.......#######.......#######..... +`, + }, + { + name: "actual", + input: input, + want: `###..#.....##..####.#..#..##..####..##.. +#..#.#....#..#.#....#.#..#..#....#.#..#. +#..#.#....#....###..##...#..#...#..#.... +###..#....#.##.#....#.#..####..#...#.##. +#....#....#..#.#....#.#..#..#.#....#..#. +#....####..###.#....#..#.#..#.####..###. +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day11/main.go b/2022/day11/main.go new file mode 100644 index 0000000..64ec5ed --- /dev/null +++ b/2022/day11/main.go @@ -0,0 +1,243 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(true) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(true) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(useRealInput bool) int { + monkeys := initInput() + if !useRealInput { + monkeys = initExample() + } + + inspectedCounts := make([]int, len(monkeys)) + for round := 0; round < 20; round++ { + for i, monkey := range monkeys { + for _, item := range monkey.items { + newItemVal := monkey.operation(item) / 3 + + if newItemVal%monkey.testDivisibleBy == 0 { + monkeys[monkey.trueMonkey].items = append( + monkeys[monkey.trueMonkey].items, newItemVal) + } else { + monkeys[monkey.falseMonkey].items = append( + monkeys[monkey.falseMonkey].items, newItemVal) + } + + } + inspectedCounts[i] += len(monkey.items) + + // empty out this monkey's items + monkeys[i].items = []int{} + } + } + + sort.Ints(inspectedCounts) + return inspectedCounts[len(inspectedCounts)-1] * inspectedCounts[len(inspectedCounts)-2] +} + +// oh my god i figured out a math-y remainder theorem-y thing myself! +func part2(useRealInput bool) int { + monkeys := initInput() + if !useRealInput { + monkeys = initExample() + } + + // the worry levels will always increase now that they're not being divided + // by 3, and we care about remainders because that's what all the tests are + // BUT we can't just mod by any monkey's testBy number, because they're all + // throwing the items around, + // so find a shared common denominator that can be used to keep the numbers + // under overflow + bigMod := 1 + for _, m := range monkeys { + bigMod *= m.testDivisibleBy + } + + inspectedCounts := make([]int, len(monkeys)) + for round := 0; round < 10000; round++ { + + for i, monkey := range monkeys { + for _, item := range monkey.items { + newItemVal := monkey.operation(item) + newItemVal %= bigMod + + if newItemVal%monkey.testDivisibleBy == 0 { + monkeys[monkey.trueMonkey].items = append( + monkeys[monkey.trueMonkey].items, newItemVal) + } else { + monkeys[monkey.falseMonkey].items = append( + monkeys[monkey.falseMonkey].items, newItemVal) + } + + } + inspectedCounts[i] += len(monkey.items) + + // empty out this monkey's items + monkeys[i].items = []int{} + } + } + + sort.Ints(inspectedCounts) + return inspectedCounts[len(inspectedCounts)-1] * inspectedCounts[len(inspectedCounts)-2] +} + +type monkey struct { + items []int + operation func(int) int + testDivisibleBy int + trueMonkey, falseMonkey int // indices +} + +// faster to manually type this than write a parser (and potentially debug) +func initInput() []monkey { + return []monkey{ + { + items: []int{50, 70, 89, 75, 66, 66}, + operation: func(old int) int { + return old * 5 + }, + testDivisibleBy: 2, + trueMonkey: 2, + falseMonkey: 1, + }, + { + items: []int{85}, + operation: func(old int) int { + return old * old + }, + testDivisibleBy: 7, + trueMonkey: 3, + falseMonkey: 6, + }, + { + items: []int{66, 51, 71, 76, 58, 55, 58, 60}, + operation: func(old int) int { + return old + 1 + }, + testDivisibleBy: 13, + trueMonkey: 1, + falseMonkey: 3, + }, + { + items: []int{79, 52, 55, 51}, + operation: func(old int) int { + return old + 6 + }, + testDivisibleBy: 3, + trueMonkey: 6, + falseMonkey: 4, + }, + { + items: []int{69, 92}, + operation: func(old int) int { + return old * 17 + }, + testDivisibleBy: 19, + trueMonkey: 7, + falseMonkey: 5, + }, + { + items: []int{71, 76, 73, 98, 67, 79, 99}, + operation: func(old int) int { + return old + 8 + }, + testDivisibleBy: 5, + trueMonkey: 0, + falseMonkey: 2, + }, + { + items: []int{82, 76, 69, 69, 57}, + operation: func(old int) int { + return old + 7 + }, + testDivisibleBy: 11, + trueMonkey: 7, + falseMonkey: 4, + }, + { + items: []int{65, 79, 86}, + operation: func(old int) int { + return old + 5 + }, + testDivisibleBy: 17, + trueMonkey: 5, + falseMonkey: 0, + }, + } +} + +func initExample() []monkey { + return []monkey{ + { + items: []int{79, 98}, + operation: func(num int) int { + return num * 19 + }, + testDivisibleBy: 23, + trueMonkey: 2, + falseMonkey: 3, + }, + { + items: []int{54, 65, 75, 74}, + operation: func(num int) int { + return num + 6 + }, + testDivisibleBy: 19, + trueMonkey: 2, + falseMonkey: 0, + }, + { + items: []int{79, 60, 97}, + operation: func(num int) int { + return num * num + }, + testDivisibleBy: 13, + trueMonkey: 1, + falseMonkey: 3, + }, + { + items: []int{74}, + operation: func(num int) int { + return num + 3 + }, + testDivisibleBy: 17, + trueMonkey: 0, + falseMonkey: 1, + }, + } +} diff --git a/2022/day11/main_test.go b/2022/day11/main_test.go new file mode 100644 index 0000000..745fe37 --- /dev/null +++ b/2022/day11/main_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "testing" +) + +var example = `` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + useRealInput bool + want int + }{ + { + name: "example", + useRealInput: false, + want: 10605, + }, + { + name: "actual", + useRealInput: true, + want: 151312, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.useRealInput); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + useRealInput bool + want int + }{ + { + name: "example", + useRealInput: false, + want: 2713310158, + }, + { + name: "actual", + useRealInput: true, + want: 51382025916, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.useRealInput); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day12/main.go b/2022/day12/main.go new file mode 100644 index 0000000..4141f74 --- /dev/null +++ b/2022/day12/main.go @@ -0,0 +1,152 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +var diffs = [4][2]int{ + {0, -1}, + {0, 1}, + {-1, 0}, + {1, 0}, +} + +func part1(input string) int { + grid := parseInput(input) + + queue := [][3]int{} +label: + for r, rows := range grid { + for c, cell := range rows { + if cell == "S" { + queue = append(queue, [3]int{r, c, 0}) + break label + } + } + } + seen := map[[2]int]bool{} + + for len(queue) > 0 { + front := queue[0] + queue = queue[1:] + if seen[[2]int{front[0], front[1]}] { + continue + } + seen[[2]int{front[0], front[1]}] = true + + if grid[front[0]][front[1]] == "E" { + return front[2] + } + for _, d := range diffs { + nextR, nextC := front[0]+d[0], front[1]+d[1] + if nextR >= 0 && nextR < len(grid) && nextC >= 0 && nextC < len(grid[0]) { + letterDiff := distanceBetweenLetters(grid[front[0]][front[1]], grid[nextR][nextC]) + + if letterDiff <= 1 { + queue = append(queue, [3]int{nextR, nextC, front[2] + 1}) + } + } + } + } + + return -1 +} + +func part2(input string) int { + grid := parseInput(input) + + queue := [][3]int{} +label: + for r, rows := range grid { + for c, cell := range rows { + if cell == "E" { + queue = append(queue, [3]int{r, c, 0}) + break label + } + } + } + seen := map[[2]int]bool{} + + for len(queue) > 0 { + front := queue[0] + queue = queue[1:] + if seen[[2]int{front[0], front[1]}] { + continue + } + seen[[2]int{front[0], front[1]}] = true + + if grid[front[0]][front[1]] == "a" { + return front[2] + } + for _, d := range diffs { + nextR, nextC := front[0]+d[0], front[1]+d[1] + if nextR >= 0 && nextR < len(grid) && nextC >= 0 && nextC < len(grid[0]) { + letterDiff := distanceBetweenLetters(grid[front[0]][front[1]], grid[nextR][nextC]) + + if letterDiff >= -1 { + queue = append(queue, [3]int{nextR, nextC, front[2] + 1}) + } + } + } + } + + return -1 +} + +func distanceBetweenLetters(x, y string) int { + if x == "S" { + x = "a" + } + if y == "S" { + y = "a" + } + if y == "E" { + y = "z" + } + if x == "E" { + x = "z" + } + + return cast.ToASCIICode(y) - cast.ToASCIICode(x) +} + +func parseInput(input string) (ans [][]string) { + for _, line := range strings.Split(input, "\n") { + ans = append(ans, strings.Split(line, "")) + } + return ans +} diff --git a/2022/day12/main_test.go b/2022/day12/main_test.go new file mode 100644 index 0000000..09c021b --- /dev/null +++ b/2022/day12/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "testing" +) + +var example = `Sabqponm +abcryxxl +accszExk +acctuvwj +abdefghi` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 31, + }, + { + name: "actual", + input: input, + want: 520, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 29, + }, + { + name: "actual", + input: input, + want: 508, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day13/main.go b/2022/day13/main.go new file mode 100644 index 0000000..db67218 --- /dev/null +++ b/2022/day13/main.go @@ -0,0 +1,139 @@ +package main + +import ( + _ "embed" + "encoding/json" + "flag" + "fmt" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + pairs := parseInput(input) + // sum all the indexes that are in the right order + // ONE INDEXED NOT ZERO + goodIndexSum := 0 + for i, pair := range pairs { + left, right := pair[0], pair[1] + if isInOrder(left, right) { + goodIndexSum += i + 1 + } + } + + return goodIndexSum +} + +func part2(input string) int { + pairs := parseInput(input) + allPackets := [][]interface{}{ + // good reminder that json.Unmarshal will convert numbers to float64... + // so need this to match for the way to isInOrder() function works.. + {[]interface{}{float64(2)}}, + {[]interface{}{float64(6)}}, + } + for _, pair := range pairs { + allPackets = append(allPackets, pair[0]) + allPackets = append(allPackets, pair[1]) + } + + sort.Slice(allPackets, func(i, j int) bool { + left, right := allPackets[i], allPackets[j] + return isInOrder(left, right) + }) + + ans := 1 + for i, p := range allPackets { + if fmt.Sprint(p) == "[[2]]" || fmt.Sprint(p) == "[[6]]" { + ans *= i + 1 + } + } + + return ans +} + +func parseInput(input string) (ans [][2][]interface{}) { + for _, packetPairs := range strings.Split(input, "\n\n") { + pairs := strings.Split(packetPairs, "\n") + ans = append(ans, [2][]interface{}{ + parseRawString(pairs[0]), + parseRawString(pairs[1]), + }) + } + return ans +} + +// will parse as JSON with elements as either int or []int... +func parseRawString(raw string) []interface{} { + ans := []interface{}{} + json.Unmarshal([]byte(raw), &ans) + return ans +} + +func isInOrder(left, right []interface{}) bool { + for l := 0; l < len(left); l++ { + if l > len(right)-1 { + return false + } + + // attempt to convert both to ints... + leftNum, isLeftNum := left[l].(float64) + rightNum, isRightNum := right[l].(float64) + + leftList, isLeftList := left[l].([]interface{}) + rightList, isRightList := right[l].([]interface{}) + if isLeftNum && isRightNum { + if leftNum != rightNum { + return leftNum < rightNum + } + } else if isLeftNum || isRightNum { + if isLeftNum { + leftList = []interface{}{leftNum} + } else if isRightNum { + rightList = []interface{}{rightNum} + } else { + panic(fmt.Sprintf("expected one num %T:%v, %T:%v", left[l], + left[l], right[l], right[l])) + } + return isInOrder(leftList, rightList) + } else { + // both lists + if !isLeftList || !isRightList { + panic(fmt.Sprintf("expected two lists %T:%v, %T:%v", left[l], + left[l], right[l], right[l])) + } + return isInOrder(leftList, rightList) + } + } + return true +} diff --git a/2022/day13/main_test.go b/2022/day13/main_test.go new file mode 100644 index 0000000..5bfa772 --- /dev/null +++ b/2022/day13/main_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "testing" +) + +var example = `[1,1,3,1,1] +[1,1,5,1,1] + +[[1],[2,3,4]] +[[1],4] + +[9] +[[8,7,6]] + +[[4,4],4,4] +[[4,4],4,4,4] + +[7,7,7,7] +[7,7,7] + +[] +[3] + +[[[]]] +[[]] + +[1,[2,[3,[4,[5,6,7]]]],8,9] +[1,[2,[3,[4,[5,6,0]]]],8,9]` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 13, + }, + { + name: "actual", + input: input, + want: 5760, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 140, + }, + { + name: "actual", + input: input, + want: 26670, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day14/main.go b/2022/day14/main.go new file mode 100644 index 0000000..186a408 --- /dev/null +++ b/2022/day14/main.go @@ -0,0 +1,189 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "math" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + matrix := parseInput(input) + originCol := 0 + for i, c := range matrix[0] { + if c == "+" { + originCol = i + } + } + + ans := 0 + for !dropSand(matrix, originCol) { + ans++ + } + + return ans +} + +func part2(input string) int { + matrix := parseInput(input) + originCol := 0 + for i, c := range matrix[0] { + if c == "+" { + originCol = i + } + matrix[len(matrix)-1][i] = "#" + } + + ans := 0 + for !dropSand(matrix, originCol) { + ans++ + // COULD incorporate this into the for loop conditional but then the + // ordering is important... must check origin cell BEFORE running + // dropSand... it's easier to read here... + if matrix[0][originCol] == "o" { + break + } + } + + return ans +} + +func parseInput(input string) (matrix [][]string) { + coordSets := [][][2]int{} + lowestCol := math.MaxInt64 + highestRow := 0 + for _, line := range strings.Split(input, "\n") { + rawCoords := strings.Split(line, " -> ") + coords := [][2]int{} + for _, rawCoord := range rawCoords { + rawNums := strings.Split(rawCoord, ",") + col, row := cast.ToInt(rawNums[0]), cast.ToInt(rawNums[1]) + coord := [2]int{ + col, row, + } + coords = append(coords, coord) + + lowestCol = mathy.MinInt(lowestCol, col) + highestRow = mathy.MaxInt(highestRow, row) + } + coordSets = append(coordSets, coords) + } + + // lowering this number to 1 makes it easier to print the matrix, which I + // used for part 1... but then needed to up it for part 2... or just have a + // massive screen and make the terminal text tiny... + ExtraLeftSpace := 200 + + highestCol := 0 + for s, set := range coordSets { + for i := range set { + coordSets[s][i][0] -= lowestCol - ExtraLeftSpace + highestCol = mathy.MaxInt(highestCol, coordSets[s][i][0]) + } + } + + matrix = make([][]string, highestRow+3) + for r := range matrix { + matrix[r] = make([]string, highestCol+ExtraLeftSpace*2) + } + + for _, set := range coordSets { + for i := 1; i < len(set); i++ { + cols := []int{set[i-1][0], set[i][0]} + rows := []int{set[i-1][1], set[i][1]} + + sort.Ints(cols) + sort.Ints(rows) + + if cols[0] == cols[1] { + for r := rows[0]; r <= rows[1]; r++ { + matrix[r][cols[0]] = "#" + } + } else if rows[0] == rows[1] { + for c := cols[0]; c <= cols[1]; c++ { + matrix[rows[0]][c] = "#" + } + } + } + } + + originCol := 500 - lowestCol + ExtraLeftSpace + // make it a plus so it's searchable in the next step... or could just + // return this value too... + matrix[0][originCol] = "+" + + for i, r := range matrix { + for j := range r { + if matrix[i][j] == "" { + matrix[i][j] = "." + } + } + } + + // printMatrix(matrix) + return matrix +} + +func printMatrix(matrix [][]string) { + for _, r := range matrix { + fmt.Println(r) + } +} + +func dropSand(matrix [][]string, originCol int) (fallsIntoAbyss bool) { + r, c := 0, originCol + + for r < len(matrix)-1 { + below := matrix[r+1][c] + diagonallyLeft := matrix[r+1][c-1] + diagonallyRight := matrix[r+1][c+1] + if below == "." { + r++ + } else if diagonallyLeft == "." { + r++ + c-- + } else if diagonallyRight == "." { + r++ + c++ + } else { + matrix[r][c] = "o" + return false + } + } + + return true +} diff --git a/2022/day14/main_test.go b/2022/day14/main_test.go new file mode 100644 index 0000000..561bf24 --- /dev/null +++ b/2022/day14/main_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "testing" +) + +var example = `498,4 -> 498,6 -> 496,6 +503,4 -> 502,4 -> 502,9 -> 494,9` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 24, + }, + { + name: "actual", + input: input, + want: 961, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 93, + }, + { + name: "actual", + input: input, + want: 26375, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day15/main.go b/2022/day15/main.go new file mode 100644 index 0000000..3b5554c --- /dev/null +++ b/2022/day15/main.go @@ -0,0 +1,174 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input, 2000000) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input, 4000000) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +// x is col, y is row +// unbounded grid... +// In the row where y=2000000, how many positions canNOT contain a beacon? +// +// very naive approach of marking each coord that is visible from some sensor +// then remove all beacons that are on the target row +// then just return the length of the map containing "seen" cells on target row +// +// this is brutally slow, no way this approach works for part 2 +func part1(input string, targetRow int) int { + pairs := parseInput(input) + + blockedCoords := map[[2]int]bool{} + for _, p := range pairs { + manhattanDist := mathy.ManhattanDistance(p.beaconRow, p.beaconCol, + p.sensorRow, p.sensorCol) + + // if target row is reachable, block coords on it... + blockable := manhattanDist - mathy.AbsInt(p.sensorRow-targetRow) + if blockable > 0 { + for i := 0; i <= blockable; i++ { + // add blocks to map in both left and right directions + blockedCoords[[2]int{ + targetRow, p.sensorCol - i, + }] = true + blockedCoords[[2]int{ + targetRow, p.sensorCol + i, + }] = true + } + } + } + + // remove any beacons that are present in the input? + for _, p := range pairs { + delete(blockedCoords, [2]int{p.beaconRow, p.beaconCol}) + } + + return len(blockedCoords) +} + +func part2(input string, coordLimit int) int { + pairs := parseInput(input) + + sensors := []parsedSensor{} + for _, p := range pairs { + sensors = append(sensors, parsedSensor{ + sensorRow: p.sensorRow, + sensorCol: p.sensorCol, + manhattanDist: mathy.ManhattanDistance(p.sensorCol, p.sensorRow, + p.beaconCol, p.beaconRow), + }) + } + + // search space is too large to iterate over the entire thing and check if + // SOME sensor can see that location... + // + // we can assume that the final resting point will be 1 cell away from the + // border of a (actually multiple) sensor. this runs under the assumption + // that there is only one answer + for _, sensor := range sensors { + distPlusOne := sensor.manhattanDist + 1 + + // checking in this pattern w/ manhattan distance of 1 + // 1 + // 2 3 + // 4 S 5 + // 6B7 + // 8 + for r := -distPlusOne; r <= distPlusOne; r++ { + targetRow := sensor.sensorRow + r + + if targetRow < 0 { + continue + } + if targetRow > coordLimit { + break + } + + // check left and right on the target row + // zero for first and last r's... then subtract or add it from the + // sensor's col + colOffset := distPlusOne - mathy.AbsInt(r) + colLeft := sensor.sensorCol - colOffset + colRight := sensor.sensorCol + colOffset + + if colLeft >= 0 && colLeft <= coordLimit && + !isReachable(sensors, colLeft, targetRow) { + return colLeft*4000000 + targetRow + } + if colRight >= 0 && colRight <= coordLimit && + !isReachable(sensors, colRight, targetRow) { + return colRight*4000000 + targetRow + } + } + } + panic("unreachable") +} + +type pair struct { + sensorRow, sensorCol int + beaconRow, beaconCol int +} + +func parseInput(input string) (ans []pair) { + // Sensor at x=2150774, y=3136587: closest beacon is at x=2561642, y=2914773 + for _, line := range strings.Split(input, "\n") { + p := pair{} + _, err := fmt.Sscanf(line, + "Sensor at x=%d, y=%d: closest beacon is at x=%d, y=%d", + &p.sensorCol, &p.sensorRow, &p.beaconCol, &p.beaconRow) + if err != nil { + panic("parsing: " + err.Error()) + } + ans = append(ans, p) + } + return ans +} + +type parsedSensor struct { + sensorRow, sensorCol int + manhattanDist int +} + +func isReachable(sensors []parsedSensor, c, r int) bool { + for _, sensor := range sensors { + // if reachable, break + if sensor.manhattanDist >= mathy.ManhattanDistance(c, r, + sensor.sensorCol, sensor.sensorRow) { + return true + } + + } + return false +} diff --git a/2022/day15/main_test.go b/2022/day15/main_test.go new file mode 100644 index 0000000..6f92558 --- /dev/null +++ b/2022/day15/main_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "testing" +) + +var example = `Sensor at x=2, y=18: closest beacon is at x=-2, y=15 +Sensor at x=9, y=16: closest beacon is at x=10, y=16 +Sensor at x=13, y=2: closest beacon is at x=15, y=3 +Sensor at x=12, y=14: closest beacon is at x=10, y=16 +Sensor at x=10, y=20: closest beacon is at x=10, y=16 +Sensor at x=14, y=17: closest beacon is at x=10, y=16 +Sensor at x=8, y=7: closest beacon is at x=2, y=10 +Sensor at x=2, y=0: closest beacon is at x=2, y=10 +Sensor at x=0, y=11: closest beacon is at x=2, y=10 +Sensor at x=20, y=14: closest beacon is at x=25, y=17 +Sensor at x=17, y=20: closest beacon is at x=21, y=22 +Sensor at x=16, y=7: closest beacon is at x=15, y=3 +Sensor at x=14, y=3: closest beacon is at x=15, y=3 +Sensor at x=20, y=1: closest beacon is at x=15, y=3` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + targetRow int + want int + }{ + { + name: "example", + input: example, + targetRow: 10, + want: 26, + }, + { + name: "actual", + input: input, + targetRow: 2000000, + want: 4560025, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input, tt.targetRow); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + coordLimit int + want int + }{ + { + name: "example", + input: example, + coordLimit: 20, + want: 56000011, + }, + { + name: "actual", + input: input, + coordLimit: 4000000, + want: 12480406634249, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input, tt.coordLimit); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day16/main.go b/2022/day16/main.go new file mode 100644 index 0000000..0f33bb7 --- /dev/null +++ b/2022/day16/main.go @@ -0,0 +1,393 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +/* +PART 1 and PART 2 ARE IMPLEMENTED COMPLETELY SEPARATELY. +If you're looking for part 2 inspiration you might want to not even look at my part1 code... +*/ + +func part1(input string) int { + // 30 min until cave erupts + // start in room AA + // creates graph to other rooms... + // flow rate is zero to start + // could spend 1 minute moving to BB and 1 more minute opening it. will release pressure of during remaining 28 of 30 minutes. 13 flow * 28 minutes = 364 total pressure lol + // then can move to CC in min 3, open in min 4, etc etc + // release max pressure in 30 min? + graph := makeGraph(input) + + open := map[string]bool{"AA": true} + for name, rm := range graph { + if rm.flowRate == 0 { + open[name] = true + } + } + + // "AA" open will not matter really... + return bfs(graph, "AA", 30, 0, open, map[string]int{}) +} + +type room struct { + name string + flowRate int + connectedTo []string +} + +func makeGraph(input string) map[string]room { + graph := map[string]room{} + + for _, line := range strings.Split(input, "\n") { + // Valve BB has flow rate=13; tunnels lead to valves CC, AA + parts := strings.Split(line, "; ") + rm := room{} + _, err := fmt.Sscanf(parts[0], "Valve %s has flow rate=%d", &rm.name, &rm.flowRate) + if err != nil { + panic("parsing valve name and flow rate" + err.Error()) + } + connections := strings.Split(parts[1], ", ") + // update first entry to remove leading string + connections[0] = connections[0][len(connections[0])-2:] + rm.connectedTo = connections + graph[rm.name] = rm + } + + return graph +} + +func bfs(graph map[string]room, currentRoom string, minutesLeft, currentPressure int, open map[string]bool, memo map[string]int) int { + if minutesLeft == 0 { + return 0 + } + + key := hash(currentRoom, minutesLeft, open, currentPressure) + if v, ok := memo[key]; ok { + return v + } + + // recursive calls will update this if it is better, then return it + bestFlow := 0 + + // there are two paths to take at a room + // 1. stay and open the valve + // this is only worth doing if the valve is not already on + // 2. move to a neighboring + + // 1. open current room's valve + if !open[currentRoom] { + open[currentRoom] = true + // totalPressureContribution := (minutesLeft - 1) * graph[currentRoom].flowRate + + newPressure := currentPressure + graph[currentRoom].flowRate + + maybeBest := currentPressure + bfs(graph, currentRoom, minutesLeft-1, newPressure, open, memo) + + bestFlow = mathy.MaxInt(bestFlow, maybeBest) + + // backtrack + open[currentRoom] = false + } + + // 2. move to neighbors + for _, neighbor := range graph[currentRoom].connectedTo { + maybeBest := currentPressure + bfs(graph, neighbor, minutesLeft-1, currentPressure, open, memo) + bestFlow = mathy.MaxInt(bestFlow, maybeBest) + } + + memo[key] = bestFlow + + return bestFlow +} + +func hash(currentRoom string, minutesLeft int, open map[string]bool, currentPressure int) string { + rms := []string{} + for k := range open { + rms = append(rms, k) + } + sort.Strings(rms) + return fmt.Sprint(currentRoom, minutesLeft, rms, currentPressure) +} + +// PART 2, basically restarting because my part1 seems too far in the opposite direction to reuse + +func part2(input string) int { + // index within [16]int arrays which will be used to track which rooms have been visited + // from analysis i know that my input has 15 non-zero pressure rooms + // every time we visit a room we'll open the valve + + graph := makeGraph(input) + + roomToFlowRate := map[string]int{} + highestFlowRatePossible := 0 // might be useful for an optimization later + + // sort room names so the arrays/slices later will be in a repeatable order (easier debugging) + // include starting room "AA" in this list even though it has zero flow rate + roomNames := []string{"AA"} + for name, room := range graph { + if room.flowRate != 0 { + roomNames = append(roomNames, name) + } + } + + // reformat into arrays/slices so we don't have to do room name lookups + flowRates := make([]int, len(roomNames)) + + sort.Strings(roomNames) + + for i, name := range roomNames { + roomToFlowRate[name] = graph[name].flowRate + flowRates[i] = roomToFlowRate[name] + highestFlowRatePossible += graph[name].flowRate + } + + weightedGraph := makeWeightedGraph(graph, roomNames) + + visitedArrayToHighestPressureTotals := map[[16]bool]int{} + dfsGetHighestPressureTotalsForEveryVisitedState(26, [16]bool{}, weightedGraph, flowRates, 0, 0, 0, visitedArrayToHighestPressureTotals) + + return highestDisjointPair(visitedArrayToHighestPressureTotals) +} + +func makeWeightedGraph(graph map[string]room, roomNames []string) [][]int { + ans := make([][]int, len(roomNames)) + for i := range ans { + ans[i] = make([]int, len(roomNames)) + } + + // bfs between every node to make graph + + for startIndex, startName := range roomNames { + for endIndex, endName := range roomNames { + if startName == endName { + continue + } + stepsBetweenRooms := bfsDistanceBetweenRooms(graph, startName, endName) + ans[startIndex][endIndex] = stepsBetweenRooms + ans[endIndex][startIndex] = stepsBetweenRooms + } + } + + return ans +} + +func bfsDistanceBetweenRooms(graph map[string]room, startName, endName string) int { + type node struct { + name string + steps int + } + + queue := []node{ + { + name: startName, + steps: 0, + }, + } + + seen := map[string]bool{} + for len(queue) > 0 { + pop := queue[0] + queue = queue[1:] + if seen[pop.name] { + continue + } + seen[pop.name] = true + + if pop.name == endName { + return pop.steps + } + for _, neighbor := range graph[pop.name].connectedTo { + queue = append(queue, node{ + name: neighbor, + steps: pop.steps + 1, + }) + } + } + // assume all rooms are reachable + panic("should return from loop") +} + +// populates ansArray with the best possible values for visiting a particular set of rooms +func dfsGetHighestPressureTotalsForEveryVisitedState(timeLeft int, visited [16]bool, + graph [][]int, flowRates []int, currentRoom int, flowRate, totalPressure int, + ansArray map[[16]bool]int) { + if timeLeft < 0 { + panic("negative timeLeft") + } + if timeLeft == 0 { + ansArray[visited] = mathy.MaxInt(ansArray[visited], totalPressure) + return + } + + // branch 1: just not moving at all + dfsGetHighestPressureTotalsForEveryVisitedState(0, visited, graph, flowRates, currentRoom, + flowRate, totalPressure+flowRate*timeLeft, ansArray) + + // rest of branches: attempt to visit every possible non-visited node + for roomIndex := range graph { + hasBeenVisited := visited[roomIndex] + if hasBeenVisited { + continue + } + + // get to room, one more to open valve + timeToOpenNextValve := graph[currentRoom][roomIndex] + 1 + // not worth visiting if the valve can't be opened in time + if timeLeft < timeToOpenNextValve { + continue + } + + // in Go this makes a full copy of the array, so &nextVisited != &visited + // this is NOT true for slices ([]bool) + nextVisited := visited + nextVisited[roomIndex] = true + dfsGetHighestPressureTotalsForEveryVisitedState( + timeLeft-timeToOpenNextValve, + nextVisited, + graph, + flowRates, + roomIndex, + flowRate+flowRates[roomIndex], + totalPressure+flowRate*timeToOpenNextValve, + ansArray) + } + +} + +func highestDisjointPair(visitedArrayToHighestPressureTotals map[[16]bool]int) int { + type finishingState struct { + bitmap int + // leaving this here to explain a simpler solution (without bitmap overkill) + // visited [16]bool + totalPressure int + } + + allFinishingStates := []finishingState{} + for visited, totalPressure := range visitedArrayToHighestPressureTotals { + allFinishingStates = append(allFinishingStates, finishingState{ + bitmap: convertToBitmap(visited), + totalPressure: totalPressure, + }) + } + + // sort in decreasing order + sort.Slice(allFinishingStates, func(i, j int) bool { + return allFinishingStates[i].totalPressure > allFinishingStates[j].totalPressure + }) + + bestCombo := -1 + + for _, baseFinishingState := range allFinishingStates { + for _, maybeDisjointFinishingState := range allFinishingStates { + pressureSum := baseFinishingState.totalPressure + maybeDisjointFinishingState.totalPressure + // allFinishingStates is sorted in decreasing order so at this point there is no reason + // to continue checking sums + if pressureSum < bestCombo { + break + } + + // only update baseFinishing state if sets are disjointed (human and elephant can't + // open the same room's valve) + // + // using bit logic is overkill, this could've been replaced with this single for loop: + // isDisjoint := true + // for i, wasVisited := range baseFinishingState.visited { + // if wasVisited && maybeDisjointFinishingState.visited[i] { + // isDisjoint = false + // break + // } + // } + if baseFinishingState.bitmap&maybeDisjointFinishingState.bitmap == 0 { + bestCombo = mathy.MaxInt(bestCombo, pressureSum) + } + + } + } + + return bestCombo +} + +func convertToBitmap(visited [16]bool) int { + var bitmap int + for i, wasVisited := range visited { + if wasVisited { + bitmap |= 1 << i + } + } + return bitmap +} + +// recheck part1 using part2 logic +func part1ViaPart2(input string) int { + graph := makeGraph(input) + + roomToFlowRate := map[string]int{} + highestFlowRatePossible := 0 // might be useful for an optimization later + + // sort room names so the arrays/slices later will be in a repeatable order (easier debugging) + // include starting room "AA" in this list even though it has zero flow rate + roomNames := []string{"AA"} + for name, room := range graph { + if room.flowRate != 0 { + roomNames = append(roomNames, name) + } + } + + // reformat into arrays/slices so we don't have to do room name lookups + flowRates := make([]int, len(roomNames)) + + sort.Strings(roomNames) + + for i, name := range roomNames { + roomToFlowRate[name] = graph[name].flowRate + flowRates[i] = roomToFlowRate[name] + highestFlowRatePossible += graph[name].flowRate + } + + weightedGraph := makeWeightedGraph(graph, roomNames) + + visitedArrayToHighestPressureTotals := map[[16]bool]int{} + dfsGetHighestPressureTotalsForEveryVisitedState(30, [16]bool{}, weightedGraph, flowRates, 0, 0, 0, visitedArrayToHighestPressureTotals) + + highest := 0 + for _, val := range visitedArrayToHighestPressureTotals { + highest = mathy.MaxInt(highest, val) + } + return highest +} diff --git a/2022/day16/main_test.go b/2022/day16/main_test.go new file mode 100644 index 0000000..56cdca8 --- /dev/null +++ b/2022/day16/main_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "testing" +) + +var example = `Valve AA has flow rate=0; tunnels lead to valves DD, II, BB +Valve BB has flow rate=13; tunnels lead to valves CC, AA +Valve CC has flow rate=2; tunnels lead to valves DD, BB +Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE +Valve EE has flow rate=3; tunnels lead to valves FF, DD +Valve FF has flow rate=0; tunnels lead to valves EE, GG +Valve GG has flow rate=0; tunnels lead to valves FF, HH +Valve HH has flow rate=22; tunnel leads to valve GG +Valve II has flow rate=0; tunnels lead to valves AA, JJ +Valve JJ has flow rate=21; tunnel leads to valve II` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 1651, + }, + { + name: "actual", + input: input, + want: 1828, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + t.Run(tt.name+"with part 2 logic", func(t *testing.T) { + if got := part1ViaPart2(tt.input); got != tt.want { + t.Errorf("part1ViaPart2() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 1707, + }, + { + name: "actual", + input: input, + want: 2292, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day17/main.go b/2022/day17/main.go new file mode 100644 index 0000000..38f4088 --- /dev/null +++ b/2022/day17/main.go @@ -0,0 +1,312 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input, 1000000000000) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + // #### + + // .#. + // ### + // .#. + + // ..# + // ..# + // ### + + // # + // # + // # + // # + + // ## + // ## + + // chamber is 7 units wide + // rocks fall w/ 2 spaces to left wall, 3 spaces to next rock or floor + // rocks pushed by wind, then fall down 1 space + // WHEN rock touches something below (ground or another settled rock), rock is "done" and next rock appears + // this also means that rocks will be pushed once L/R before fully settling (unless blocked L/R) + + s := newState(input) + for i := 0; i < 2022; i++ { + s.dropRock() + } + + // height after 2022 rocks fall + return s.highestSettledRow + 1 // 1 indexed +} + +func part2(input string, wantedRocks int) int { + s := newState(input) + // obviously can't do the calculations literally anymore... + // need some kind of "last time this state was seen" check to skip steps + // need to hash states + // - the top 10ish rows should be plenty + // - rock index + // - value = steps since last seen AND highestRow to calc skips + + // [2]int{steps when last seen, rocks dropped, highestRow to calc diff} + pastStates := map[string][3]int{} + + // will keep track of rows that are mathematically skipped otherwise it'll + // mess with state and the settled matrix + dupeRows := 0 + + rocksDropped := 0 + for rocksDropped < wantedRocks { + s.dropRock() + rocksDropped++ + + h := s.hash(20) + if past, ok := pastStates[h]; ok { + pastSteps, pastRocksDropped, pastHighRow := past[0], past[1], past[2] + + stepsToSkip := s.stepIndex - pastSteps + rocksToSkip := rocksDropped - pastRocksDropped + rowsToAdd := s.highestSettledRow - pastHighRow + + iterationsToSkip := (wantedRocks - rocksDropped) / rocksToSkip + dupeRows += rowsToAdd * iterationsToSkip + s.stepIndex += stepsToSkip * iterationsToSkip + rocksDropped += rocksToSkip * iterationsToSkip + } else { + pastStates[h] = [3]int{ + s.stepIndex, rocksDropped, s.highestSettledRow, + } + } + } + + // height after 2022 rocks fall + return s.highestSettledRow + 1 + dupeRows // 1 indexed +} + +type state struct { + settled [][]string + highestSettledRow int + fallingCoords [][2]int + nextRockIndex int + steps []string + stepIndex int +} + +func newState(input string) state { + s := state{ + settled: [][]string{}, + highestSettledRow: -1, + fallingCoords: nil, + nextRockIndex: 0, + steps: strings.Split(input, ""), + stepIndex: 0, + } + + return s +} + +// knew I'd need this for debugging... +func (s state) printState() { + copySettled := [][]string{} + for _, row := range s.settled { + copyRow := make([]string, len(row)) + copy(copyRow, row) + copySettled = append(copySettled, copyRow) + } + + for _, coord := range s.fallingCoords { + copySettled[coord[0]][coord[1]] = "@" + } + + var sb strings.Builder + for r := len(copySettled) - 1; r >= 0; r-- { + sb.WriteString(strings.Join(copySettled[r], "") + cast.ToString(r) + "\n") + } + fmt.Println(sb.String()) +} + +func (s *state) dropRock() { + s.populateNextBaseCoords() + + highestRow := 0 + for _, c := range s.fallingCoords { + highestRow = mathy.MaxInt(highestRow, c[0]) + } + for len(s.settled) <= highestRow { + s.settled = append(s.settled, newEmptyRow()) + } + + // will be set back to nil when settled + for s.fallingCoords != nil { + switch s.steps[s.stepIndex%len(s.steps)] { + case ">": + // check if can move right + canMoveRight := true + for _, c := range s.fallingCoords { + if c[1] == 6 || s.settled[c[0]][c[1]+1] != "." { + canMoveRight = false + } + } + if canMoveRight { + for i := range s.fallingCoords { + s.fallingCoords[i][1]++ + } + } + case "<": + // check if can move left + canMoveLeft := true + for _, c := range s.fallingCoords { + if c[1] == 0 || s.settled[c[0]][c[1]-1] != "." { + canMoveLeft = false + } + } + if canMoveLeft { + for i := range s.fallingCoords { + s.fallingCoords[i][1]-- + } + } + default: + panic(s.steps[s.stepIndex]) + } + s.stepIndex++ + + // move down + canMoveDown := true + for _, c := range s.fallingCoords { + if c[0] == 0 || s.settled[c[0]-1][c[1]] != "." { + canMoveDown = false + } + } + // is blocked, draw onto settled then make nil + if !canMoveDown { + for _, c := range s.fallingCoords { + s.settled[c[0]][c[1]] = "#" + } + s.fallingCoords = nil + + for r := len(s.settled) - 1; r >= 0; r-- { + if strings.Join(s.settled[r], "") != "......." { + s.highestSettledRow = r + break + } + } + } else { + for i := range s.fallingCoords { + s.fallingCoords[i][0]-- + } + } + } +} + +func newEmptyRow() []string { + row := make([]string, 7) + for i := range row { + row[i] = "." + } + return row +} + +var baseCoords = [][][2]int{ + { + // line #### + {0, 0}, + {0, 1}, + {0, 2}, + {0, 3}, + }, { + // plus + {0, 1}, + {1, 0}, + {1, 1}, + {1, 2}, + {2, 1}, + }, { + // flipped L + {0, 0}, + {0, 1}, + {0, 2}, + {1, 2}, + {2, 2}, + }, { + // vert line + {0, 0}, + {1, 0}, + {2, 0}, + {3, 0}, + }, { + // square + {0, 0}, + {0, 1}, + {1, 0}, + {1, 1}, + }, +} + +func init() { + // add 2 cols to all baseCoords because they fall 2 off of left wall + for i := range baseCoords { + for j := range baseCoords[i] { + baseCoords[i][j][1] += 2 + } + } +} + +func (s *state) populateNextBaseCoords() { + copyCoords := make([][2]int, len(baseCoords[s.nextRockIndex])) + copy(copyCoords, baseCoords[s.nextRockIndex]) + s.nextRockIndex++ + s.nextRockIndex %= 5 + + // lowest row of baseCoords... + + for i := range copyCoords { + copyCoords[i][0] += s.highestSettledRow + 1 + 3 + } + s.fallingCoords = copyCoords +} + +// for part 2 to find return states +// NOTE: had to play with the number of rows to be hashed... 20 seems to +// work on the example input +func (s *state) hash(topRowsToHash int) string { + var sb strings.Builder + sb.WriteString(cast.ToString(s.nextRockIndex)) + for r := s.highestSettledRow; r >= 0 && r > s.highestSettledRow-topRowsToHash; r-- { + sb.WriteString("\n" + strings.Join(s.settled[r], "")) + } + return sb.String() +} diff --git a/2022/day17/main_test.go b/2022/day17/main_test.go new file mode 100644 index 0000000..06a5fef --- /dev/null +++ b/2022/day17/main_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "testing" +) + +var example = `>>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 3068, + }, + { + name: "actual", + input: input, + want: 3219, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + wantedRocks int + want int + }{ + // part 1 values should work as well, so using them for extra testing + { + name: "example", + input: example, + wantedRocks: 2022, + want: 3068, + }, + { + name: "actual", + input: input, + wantedRocks: 2022, + want: 3219, + }, + { + name: "example", + input: example, + wantedRocks: 1000000000000, + want: 1514285714288, + }, + { + name: "actual", + input: input, + wantedRocks: 1000000000000, + want: 1582758620701, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input, tt.wantedRocks); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day18/main.go b/2022/day18/main.go new file mode 100644 index 0000000..adea845 --- /dev/null +++ b/2022/day18/main.go @@ -0,0 +1,171 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +// to six adjacent face... +var diffs = [][3]int{ + {0, 0, 1}, + {0, 1, 0}, + {1, 0, 0}, + {0, 0, -1}, + {0, -1, 0}, + {-1, 0, 0}, +} + +func part1(input string) int { + rawCoords := parseInput(input) + mapCoords := convertRawCoordsToMap(rawCoords) + + totalSurfaceArea := 0 + for _, coord := range rawCoords { + neighbors := 6 + for _, d := range diffs { + if mapCoords[[3]int{ + coord[0] - d[0], + coord[1] - d[1], + coord[2] - d[2], + }] { + neighbors-- + } + } + totalSurfaceArea += neighbors + } + + return totalSurfaceArea +} + +func part2(input string) int { + rawCoords := parseInput(input) + mapCoords := convertRawCoordsToMap(rawCoords) + + // get bounds + var limitX, limitY, limitZ int + for c := range mapCoords { + limitX = mathy.MaxInt(limitX, c[0]) + limitY = mathy.MaxInt(limitY, c[1]) + limitZ = mathy.MaxInt(limitZ, c[2]) + } + + // bfs to see if an edge can be reached + // delete if not useful + + totalExternalSurfaceArea := 0 + + for coord := range mapCoords { + totalExternalSurfaceArea += facesThatCanReachEdge(coord, mapCoords, + limitX, limitY, limitZ) + } + + // too low: 1036 + return totalExternalSurfaceArea +} + +func parseInput(input string) (ans [][3]int) { + for _, line := range strings.Split(input, "\n") { + parts := strings.Split(line, ",") + ans = append(ans, [3]int{ + cast.ToInt(parts[0]), + cast.ToInt(parts[1]), + cast.ToInt(parts[2]), + }) + } + return ans +} +func convertRawCoordsToMap(rawCoords [][3]int) map[[3]int]bool { + set := map[[3]int]bool{} + for _, coord := range rawCoords { + set[coord] = true + } + return set +} + +// there would be a big optimization here to keep track of all coords that have +// a known path to an edge, that would eliminate a lot of duplicate work... but +// i think this is a small enough problem space to ignore that... +func facesThatCanReachEdge(coord [3]int, set map[[3]int]bool, limitX, limitY, limitZ int) int { + ans := 0 + for _, d := range diffs { + next := [3]int{ + coord[0] + d[0], + coord[1] + d[1], + coord[2] + d[2], + } + + reachResult := canReachEdge(next, set, limitX, limitY, limitZ) + if reachResult { + ans++ + } + } + + return ans +} + +func canReachEdge(coord [3]int, set map[[3]int]bool, limitX, limitY, limitZ int, +) bool { + queue := [][3]int{coord} + seen := map[[3]int]bool{} + for len(queue) > 0 { + front := queue[0] + queue = queue[1:] + + // seen already or hit some other droplet, skip + if seen[front] || set[front] { + continue + } + seen[front] = true + + // edge reached + if front[0] <= 0 || front[0] >= limitX || + front[1] <= 0 || front[1] >= limitY || + front[2] <= 0 || front[2] >= limitZ { + return true + } + + for _, d := range diffs { + next := [3]int{ + front[0] + d[0], + front[1] + d[1], + front[2] + d[2], + } + queue = append(queue, next) + } + } + return false +} diff --git a/2022/day18/main_test.go b/2022/day18/main_test.go new file mode 100644 index 0000000..5947ef8 --- /dev/null +++ b/2022/day18/main_test.go @@ -0,0 +1,99 @@ +package main + +import ( + "testing" +) + +var example = `2,2,2 +1,2,2 +3,2,2 +2,1,2 +2,3,2 +2,2,1 +2,2,3 +2,2,4 +2,2,6 +1,2,5 +3,2,5 +2,1,5 +2,3,5` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 64, + }, + { + name: "actual", + input: input, + want: 4636, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +var flatDisc = `5,5,5 +5,5,6 +5,5,7 +5,6,5 +5,6,6 +5,6,7 +5,7,5 +5,7,6 +5,7,7` + +// 3x3x1 disc... +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "dumb simple", + input: "1,1,1\n2,1,1", + want: 10, + }, + { + name: "dumber simpleer", + input: "2,1,1", + want: 6, + }, + { + name: "example", + input: example, + want: 58, + }, + { + name: "flatDisc", + input: flatDisc, + // 9 + 9 + 3 * 4 = 30 + want: 30, + }, + { + name: "actual", + input: input, + // PAIN, used coord instead of front in the bfs check :/ + want: 2572, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day19/main.go b/2022/day19/main.go new file mode 100644 index 0000000..fcbf43e --- /dev/null +++ b/2022/day19/main.go @@ -0,0 +1,211 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + blueprints := parseInput(input) + + // how many geodes can be opened in 24 minutes? + sum := 0 + for _, bp := range blueprints { + st := newState(bp) + geodesMade := st.calcMostGeodes(0, map[string]int{}, 24, 24) + // fmt.Println("ID:", bp.id, geodesMade) + sum += st.blueprint.id * geodesMade + } + + // total quality of all blueprints, quality = id * (# geodes in 24 min) + return sum +} + +func part2(input string) int { + blueprints := parseInput(input) + if len(blueprints) > 3 { + blueprints = blueprints[:3] + } + + prod := 1 + for _, bp := range blueprints { + st := newState(bp) + geodesMade := st.calcMostGeodes(0, map[string]int{}, 32, 32) + // fmt.Println(bp.id, geodesMade) + prod *= geodesMade + } + + // total quality of all blueprints, quality = id * (# geodes in 24 min) + return prod +} + +type blueprint struct { + id int + oreForOreRobot int + oreForClayRobot int + oreForObsidianRobot, clayForObsidianRobot int + oreForGeodeRobot, obsidianForGeodeRobot int +} + +type state struct { + blueprint + ore, clay, obsidian, geode int + oreRobots, clayRobots, obsidianRobots, geodeRobots int +} + +func newState(blueprint blueprint) state { + return state{ + blueprint: blueprint, + oreRobots: 1, + } +} + +func (s *state) farm() { + s.ore += s.oreRobots + s.clay += s.clayRobots + s.obsidian += s.obsidianRobots + s.geode += s.geodeRobots +} + +func (s *state) hash(time int) string { + return fmt.Sprint(time, s.ore, s.clay, s.obsidian, + s.geode, s.oreRobots, s.clayRobots, s.obsidianRobots, s.geodeRobots) +} + +// NOT A POINTER METHOD SO A COPY CAN BE MADE +// this is some cheeky Go struct copying, it'd be easier to read if it was just +// directly recreating all the fields +func (s state) copy() state { + return s +} + +func (s *state) calcMostGeodes(time int, memo map[string]int, totalTime int, earliestGeode int) int { + if time == totalTime { + return s.geode + } + + h := s.hash(time) + if v, ok := memo[h]; ok { + return v + } + + if s.geode == 0 && time > earliestGeode { + return 0 + } + + // factory can try to make any possible robot, will backtrack if necessary + mostGeodes := s.geode + + // always make geode robots + if s.ore >= s.oreForGeodeRobot && + s.obsidian >= s.obsidianForGeodeRobot { + cp := s.copy() + + cp.farm() + + cp.ore -= cp.oreForGeodeRobot + cp.obsidian -= cp.obsidianForGeodeRobot + cp.geodeRobots++ + if cp.geodeRobots == 1 { + earliestGeode = mathy.MinInt(earliestGeode, time+1) + } + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + + memo[h] = mostGeodes + return mostGeodes + } + + if time <= totalTime-16 && + s.oreRobots < s.oreForObsidianRobot*2 && + s.ore >= s.oreForOreRobot { + cp := s.copy() + cp.ore -= cp.oreForOreRobot + + cp.farm() + + cp.oreRobots++ + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + } + if time <= totalTime-8 && + s.clayRobots < s.clayForObsidianRobot && + s.ore >= s.oreForClayRobot { + cp := s.copy() + cp.ore -= cp.oreForClayRobot + + cp.farm() + + cp.clayRobots++ + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + } + if time <= totalTime-4 && + s.obsidianRobots < s.obsidianForGeodeRobot && + s.ore >= s.oreForObsidianRobot && s.clay >= s.clayForObsidianRobot { + + cp := s.copy() + cp.ore -= cp.oreForObsidianRobot + cp.clay -= cp.clayForObsidianRobot + cp.farm() + + cp.obsidianRobots++ + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + } + + // or no factory production this minute + cp := s.copy() + cp.ore += cp.oreRobots + cp.clay += cp.clayRobots + cp.obsidian += cp.obsidianRobots + cp.geode += cp.geodeRobots + mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode)) + + memo[h] = mostGeodes + return mostGeodes +} + +func parseInput(input string) (ans []blueprint) { + // Blueprint 1: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 2 ore and 12 obsidian. + for _, line := range strings.Split(input, "\n") { + bp := blueprint{} + _, err := fmt.Sscanf(line, "Blueprint %d: Each ore robot costs %d ore. Each clay robot costs %d ore. Each obsidian robot costs %d ore and %d clay. Each geode robot costs %d ore and %d obsidian.", + &bp.id, &bp.oreForOreRobot, &bp.oreForClayRobot, &bp.oreForObsidianRobot, + &bp.clayForObsidianRobot, &bp.oreForGeodeRobot, &bp.obsidianForGeodeRobot) + if err != nil { + panic("parsing: " + err.Error()) + } + ans = append(ans, bp) + } + return ans +} diff --git a/2022/day19/main_test.go b/2022/day19/main_test.go new file mode 100644 index 0000000..93eccc2 --- /dev/null +++ b/2022/day19/main_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "testing" +) + +var example = `Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian. +Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 3 ore and 12 obsidian.` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 33, + }, + { + name: "actual", + input: input, + want: 1127, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 56 * 62, + }, + // I actually have zero idea how long this took to run, I ran it overnight + // 21 27 38 + // { + // name: "actual", + // input: input, + // want: 21546, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day20/main.go b/2022/day20/main.go new file mode 100644 index 0000000..f725597 --- /dev/null +++ b/2022/day20/main.go @@ -0,0 +1,165 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +const part2DecryptionKey = 811589153 + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := mixList(input, 1, 1) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := mixList(input, 811589153, 10) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func mixList(input string, decryptionKey, mixes int) int { + zeroNode, originalOrder := parseInput(input) + + for i := 0; i < len(originalOrder); i++ { + originalOrder[i].val *= decryptionKey + } + + for i := 0; i < mixes; i++ { + for _, node := range originalOrder { + node.move(len(originalOrder)) + } + } + + // sum 3 vals from value zero, 1000th, 2000th, 3000th away + + return getNodeXStepsAway(zeroNode, 1000, len(originalOrder)).val + + getNodeXStepsAway(zeroNode, 2000, len(originalOrder)).val + + getNodeXStepsAway(zeroNode, 3000, len(originalOrder)).val +} + +func getNodeXStepsAway(node *llNode, steps int, listLength int) *llNode { + if steps < 0 { + panic("negative steps") + } + + iter := node + for steps > 0 { + iter = iter.next + steps-- + } + return iter +} + +type llNode struct { + val int + prev, next *llNode +} + +func (n *llNode) move(totalLength int) { + steps := n.val + // fmt.Println("before steps", steps, "total", totalLength) + steps %= totalLength - 1 + + if steps == 0 { + // fmt.Println("zero steps") + return + } + + // find slot to fit into + + if steps < 0 { + steps += (totalLength - 1) + } + // fmt.Println("modded steps", steps) + + oldPrev, oldNext := n.prev, n.next + oldPrev.next = oldNext + oldNext.prev = oldPrev + + iter := n + for steps > 0 { + // fmt.Println("steps left", steps, "iter", iter) + iter = iter.next + if iter == n { + panic("repeat") + } + steps-- + } + + nextPrev, nextNext := iter, iter.next + // fmt.Println("nextPrev & nextNext", nextPrev, nextNext) + nextPrev.next = n + n.prev = nextPrev + nextNext.prev = n + n.next = nextNext +} + +func parseInput(input string) (zeroNode *llNode, originalOrder []*llNode) { + nums := []int{} + for _, line := range strings.Split(input, "\n") { + nums = append(nums, cast.ToInt(line)) + } + + var head, iter *llNode + for _, n := range nums { + node := &llNode{ + val: n, + prev: iter, + } + if head == nil { + head = node + iter = node + } else { + iter.next = node + iter = iter.next + } + + if iter.val == 0 { + zeroNode = iter + } + originalOrder = append(originalOrder, node) + } + + head.prev = iter + iter.next = head + + return zeroNode, originalOrder +} + +// for debugging +func listToString(head *llNode, listLength int) string { + var sb strings.Builder + for listLength > 0 { + sb.WriteString(cast.ToString(head.val) + ",") + head = head.next + listLength-- + } + return sb.String() +} + +func printList(head *llNode, listLength int) { + fmt.Println(listToString(head, listLength)) +} diff --git a/2022/day20/main_test.go b/2022/day20/main_test.go new file mode 100644 index 0000000..b1a44da --- /dev/null +++ b/2022/day20/main_test.go @@ -0,0 +1,115 @@ +package main + +import ( + _ "embed" + "testing" +) + +var example = `1 +2 +-3 +3 +-2 +0 +4` +var example2 = `1 +2 +-3 +9 +-2 +0 +4` +var example3 = `1 +2 +-3 +3 +-8 +0 +4` + +func Test_mixList(t *testing.T) { + tests := []struct { + name string + input string + decryptionKey, mixes int // for part 2 mostly + want int + }{ + { + name: "example", + input: example, + decryptionKey: 1, + mixes: 1, + want: 3, + }, + { + name: "example2", + input: example2, + decryptionKey: 1, + mixes: 1, + want: 3, + }, + { + name: "example3", + input: example3, + decryptionKey: 1, + mixes: 1, + want: 3, + }, + { + name: "actual", + input: input, + decryptionKey: 1, + mixes: 1, + want: 9945, + }, + { + name: "example", + input: example, + decryptionKey: part2DecryptionKey, + mixes: 10, + want: 1623178306, + }, + { + name: "actual", + input: input, + decryptionKey: part2DecryptionKey, + mixes: 10, + want: 3338877775442, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := mixList(tt.input, tt.decryptionKey, tt.mixes); got != tt.want { + t.Errorf("mixList() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_llNode_move(t *testing.T) { + zeroNode, nodeSlice := parseInput(`0 +1 +2 +3 +4 +5`) + + zeroNode.move(len(nodeSlice)) + originalString := "0,1,2,3,4,5," + // should be the same + if got := listToString(zeroNode, len(nodeSlice)); got != originalString { + t.Errorf("moving zero, want no change %q, got %q", originalString, got) + } + + zeroNode.prev.move(len(nodeSlice)) + if got := listToString(zeroNode, len(nodeSlice)); got != originalString { + t.Errorf("moving 5, want no change %q, got %q", originalString, got) + } + + oneNode := zeroNode.next + oneNode.move(len(nodeSlice)) + want := "0,2,1,3,4,5," + if got := listToString(zeroNode, len(nodeSlice)); got != want { + t.Errorf("moving 1, want %q got %q", want, got) + } +} diff --git a/2022/day21/main.go b/2022/day21/main.go new file mode 100644 index 0000000..fb18519 --- /dev/null +++ b/2022/day21/main.go @@ -0,0 +1,190 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "regexp" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + parsed := parseInput(input) + + // what will monkey 'root' yell? + v, _ := bfs("root", parsed, map[string]int{}) + return v +} + +var numRegexp = regexp.MustCompile("^[0-9]+$") + +func bfs(key string, raw map[string]string, solved map[string]int) (int, error) { + if v, ok := solved[key]; ok { + return v, nil + } + + if numRegexp.MatchString(raw[key]) { + solved[key] = cast.ToInt(raw[key]) + return solved[key], nil + } + + equation := raw[key] + parts := strings.Split(equation, " ") + + if len(parts) != 3 { + return 0, fmt.Errorf("expected 3 parts for %q, got %q", key, equation) + } + + left, err := bfs(parts[0], raw, solved) + if err != nil { + return 0, err + } + right, err := bfs(parts[2], raw, solved) + if err != nil { + return 0, err + } + + switch parts[1] { + case "+": + solved[key] = left + right + case "-": + solved[key] = left - right + case "*": + solved[key] = left * right + case "/": + solved[key] = left / right + default: + panic("error with key: " + key + " string: " + equation) + } + return solved[key], nil +} + +func part2(input string) int { + raw := parseInput(input) + if len(strings.Split(raw["root"], " ")) != 3 { + panic(fmt.Sprintf("expected 3 parts to %q", raw["root"])) + } + + // change humn to something that will error in bfs so we know which branch + // of the equations is fully solvable + raw["humn"] = "humn_will_error_in_bfs" + + // basically making the root equation leftSymbol / rightSymbol = 1 in the + // inverted graph + invertedGraph := map[string]string{"root": "1"} + rootParts := strings.Split(raw["root"], " ") + rootParts[1] = "/" + raw["root"] = strings.Join(rootParts, " ") + + keyToInvert := "root" + solvedMap := map[string]int{} + + for keyToInvert != "humn" { + // find the equation, determine which side is easily solvable, and which + // is not, reverse the equation for the unsolvable variable (aka the one + // that needs to know what value humn shouts) + // end at humn + eq := raw[keyToInvert] + parts := strings.Split(eq, " ") + + leftRaw, rightRaw := parts[0], parts[2] + + leftVal, errLeft := bfs(leftRaw, raw, solvedMap) + if errLeft == nil { + invertedGraph[leftRaw] = cast.ToString(leftVal) + } + rightVal, errRight := bfs(rightRaw, raw, solvedMap) + if errRight == nil { + invertedGraph[rightRaw] = cast.ToString(rightVal) + } + + switch parts[1] { + case "+": + if errLeft != nil { + invertedGraph[leftRaw] = fmt.Sprintf("%s - %s", keyToInvert, rightRaw) + keyToInvert = leftRaw + } else if errRight != nil { + invertedGraph[rightRaw] = fmt.Sprintf("%s - %s", keyToInvert, leftRaw) + keyToInvert = rightRaw + } else { + panic(fmt.Sprintf("both vals did not error '+' %q: %q", keyToInvert, eq)) + } + case "-": + if errLeft != nil { + invertedGraph[leftRaw] = fmt.Sprintf("%s + %s", keyToInvert, rightRaw) + keyToInvert = leftRaw + } else if errRight != nil { + invertedGraph[rightRaw] = fmt.Sprintf("%s - %s", leftRaw, keyToInvert) + keyToInvert = rightRaw + } else { + panic(fmt.Sprintf("both vals did not error '-' %q: %q", keyToInvert, eq)) + } + case "*": + if errLeft != nil { + invertedGraph[leftRaw] = fmt.Sprintf("%s / %s", keyToInvert, rightRaw) + keyToInvert = leftRaw + } else if errRight != nil { + invertedGraph[rightRaw] = fmt.Sprintf("%s / %s", keyToInvert, leftRaw) + keyToInvert = rightRaw + } else { + panic(fmt.Sprintf("both vals did not error '/' %q: %q", keyToInvert, eq)) + } + case "/": + if errLeft != nil { + invertedGraph[leftRaw] = fmt.Sprintf("%s * %s", keyToInvert, rightRaw) + keyToInvert = leftRaw + } else if errRight != nil { + invertedGraph[rightRaw] = fmt.Sprintf("%s / %s", leftRaw, keyToInvert) + keyToInvert = rightRaw + } else { + panic(fmt.Sprintf("both vals did not error '*' %q: %q", keyToInvert, eq)) + } + + default: + panic(fmt.Sprintf("inverting graph: key: %q, eq: %q", keyToInvert, eq)) + } + } + + v, _ := bfs("humn", invertedGraph, map[string]int{}) + return v +} + +func parseInput(input string) map[string]string { + ans := map[string]string{} + for _, line := range strings.Split(input, "\n") { + parts := strings.Split(line, ": ") + ans[parts[0]] = parts[1] + } + return ans +} diff --git a/2022/day21/main_test.go b/2022/day21/main_test.go new file mode 100644 index 0000000..454181c --- /dev/null +++ b/2022/day21/main_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "testing" +) + +var example = `root: pppw + sjmn +dbpl: 5 +cczh: sllz + lgvd +zczc: 2 +ptdq: humn - dvpt +dvpt: 3 +lfqf: 4 +humn: 5 +ljgn: 2 +sjmn: drzm * dbpl +sllz: 4 +pppw: cczh / lfqf +lgvd: ljgn * ptdq +drzm: hmdt - zczc +hmdt: 32` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 152, + }, + { + name: "actual", + input: input, + want: 194501589693264, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 301, + }, + { + name: "actual", + input: input, + want: 3887609741189, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day22/main.go b/2022/day22/main.go new file mode 100644 index 0000000..beeda69 --- /dev/null +++ b/2022/day22/main.go @@ -0,0 +1,464 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + matrix, path := parseInput(input) + var row, col int + for c := 0; c < len(matrix[0]); c++ { + if matrix[0][c] == "." { + col = c + break + } + } + + diffIndex := 0 + diffs := [][2]int{ + {0, 1}, // start facing right + {1, 0}, // turning right will point you down + {0, -1}, // left + {-1, 0}, // turning left from index 0 makes you face up + } + + // walking over the edge wraps you around within the same direction... + // unless if it is a wall, then you're stuck + + p := 0 + for p < len(path) { + // get next direction... + indexOfLorR := p + for indexOfLorR < len(path) && + path[indexOfLorR] != 'L' && path[indexOfLorR] != 'R' { + indexOfLorR++ + } + steps := cast.ToInt(path[p:indexOfLorR]) + // try to move that many steps + for s := 0; s < steps; s++ { + diff := diffs[diffIndex] + nextRow, nextCol := row+diff[0], col+diff[1] + // mod them so they wrap if necessary + nextRow += len(matrix) + nextCol += len(matrix[0]) + + nextRow %= len(matrix) + nextCol %= len(matrix[0]) + + // if it's an empty space you need to keep looping around... + for matrix[nextRow][nextCol] == " " || matrix[nextRow][nextCol] == "" { + nextRow += diff[0] + nextCol += diff[1] + + // wrapping math... + nextRow += len(matrix) + nextCol += len(matrix[0]) + + nextRow %= len(matrix) + nextCol %= len(matrix[0]) + } + + // wall: break + if matrix[nextRow][nextCol] == "#" { + break + } + row = nextRow + col = nextCol + } + + if indexOfLorR == len(path) { + break + } + // handle turn if indexOfLorR is still in bounds + switch path[indexOfLorR] { + case 'L': + diffIndex-- + case 'R': + diffIndex++ + } + diffIndex += 4 + diffIndex %= 4 + p = indexOfLorR + 1 + } + + // final row, col, facing + // row & col are 1 indexed + // facing is indexed same as diffs slice + // 1000 * row + 4 * col + facing_index + return 1000*(row+1) + 4*(col+1) + diffIndex +} + +func parseInput(input string) ([][]string, string) { + parts := strings.Split(input, "\n\n") + + matrix := [][]string{} + topRowLen := len(strings.Split(parts[0], "\n")[0]) + + for _, line := range strings.Split(parts[0], "\n") { + matrix = append(matrix, make([]string, topRowLen)) + split := strings.Split(line, "") + copy(matrix[len(matrix)-1], split) + } + + return matrix, parts[1] +} + +func part2(input string) int { + matrix, path := parseInput(input) + var row, col int + for c := 0; c < len(matrix[0]); c++ { + if matrix[0][c] == "." { + col = c + break + } + } + + diffIndex := 0 + diffs := [][2]int{ + {0, 1}, // start facing right + {1, 0}, // turning right will point you DOWN + {0, -1}, // left + {-1, 0}, // turning left from index 0 makes you face up + } + + // shape of example + // # + // ### + // ## + // + // + // shape of my input + // ## + // # + // ## + // # + + // a lot of (hah) edge case handling to determine where to "teleport" to + // pen and paper math... might not be worth doing the example input + // because i'm going to make a literal calculation for my input shape... + + p := 0 + for p < len(path) { + // get next direction... + indexOfLorR := p + for indexOfLorR < len(path) && + path[indexOfLorR] != 'L' && path[indexOfLorR] != 'R' { + indexOfLorR++ + } + steps := cast.ToInt(path[p:indexOfLorR]) + + // try to move that many steps + for s := 0; s < steps; s++ { + diff := diffs[diffIndex] + nextRow, nextCol := row+diff[0], col+diff[1] + + // DO NOT UPDATE diffIndex here because if it's a wall we DON'T want + // to change directions + nextRow, nextCol, nextDiffIndex := handleWrap(row, col, nextRow, nextCol, diffIndex) + + // we'll never see empty spaces now because we're handling wrapping above + // wall: break + if matrix[nextRow][nextCol] == "#" { + break + } + // only update if we didn't hit a wall + row = nextRow + col = nextCol + diffIndex = nextDiffIndex + } + + if indexOfLorR == len(path) { + break + } + // handle turn if indexOfLorR is still in bounds + switch path[indexOfLorR] { + case 'L': + diffIndex-- + case 'R': + diffIndex++ + } + diffIndex += 4 + diffIndex %= 4 + p = indexOfLorR + 1 + } + + // final answer calculated from flattened map coords + // 1000 * row + 4 * col + facing_index + // too low: 111043 + return 1000*(row+1) + 4*(col+1) + diffIndex +} + +// handles edge cases ;) +// how i'll number my boxes... +// 21 +// 3 +// 54 +// 6 + +const ( + RightIndex = 0 + DownIndex = 1 + LeftIndex = 2 + UpIndex = 3 +) + +// handleWrap checks if the movement from r,c to nextRow, nextCol is off the +// edge of the matrix, if so it does the maths and direction change to wrap +// around the edge of the cube, this is very manual and based upon a drawing i +// made of my input (i'll upload it when i remember...) +// +// got a little carried away with assertions in here trying to debug... +func handleWrap(r, c, nextRow, nextCol, diffIndex int) (newRow, newCol, newDiffIndex int) { + // there will be roughly 14 checks in here... this is gonna get ugly, esp + // b/c i'm too lazy to dry this up + + // 2 -> 5 conversion + if getBoxNumber(r, c) == 2 && + 0 <= nextRow && nextRow < 50 && nextCol == 49 { + if diffIndex != LeftIndex { + panic(fmt.Sprintf("expected LeftIndex, got %d", diffIndex)) + } + + newCol = 0 + newRow = 149 - nextRow + if getBoxNumber(newRow, newCol) != 5 { + panic(fmt.Sprintf("expected to move to box 5, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, RightIndex + } + // 5 -> 2 + if getBoxNumber(r, c) == 5 && + nextCol == -1 && 100 <= nextRow && nextRow < 150 { + if diffIndex != LeftIndex { + panic(fmt.Sprintf("expected LeftIndex got %d", diffIndex)) + } + newCol = 50 + newRow = 149 - nextRow + if getBoxNumber(newRow, newCol) != 2 { + panic(fmt.Sprintf("expected to move to box 2, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, RightIndex + } + + // 3 -> 5 + if getBoxNumber(r, c) == 3 && + nextCol == 49 && 50 <= nextRow && nextRow < 100 { + if diffIndex != LeftIndex { + panic(fmt.Sprintf("expected LeftIndex, got %d", diffIndex)) + } + newRow = 100 + newCol = nextRow - 50 + if getBoxNumber(newRow, newCol) != 5 { + panic(fmt.Sprintf("expected to move to box 5, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, DownIndex + } + // 5 -> 3 + if getBoxNumber(r, c) == 5 && + nextRow == 99 && 0 <= nextCol && nextCol < 50 { + if diffIndex != UpIndex { + panic(fmt.Sprintf("expected UpIndex, got %d", diffIndex)) + } + newRow = nextCol + 50 + newCol = 50 + if getBoxNumber(newRow, newCol) != 3 { + panic(fmt.Sprintf("expected to move to box 3, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, RightIndex + } + + // 2 -> 6 + if getBoxNumber(r, c) == 2 && + nextRow == -1 && 50 <= nextCol && nextCol < 100 { + if diffIndex != UpIndex { + panic(fmt.Sprintf("expected UpIndex, got %d", diffIndex)) + } + newRow = nextCol + 100 + newCol = 0 + if getBoxNumber(newRow, newCol) != 6 { + panic(fmt.Sprintf("expected to move to box 6, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, RightIndex + } + // 6 -> 2 + if getBoxNumber(r, c) == 6 && + nextCol == -1 && 150 <= nextRow && nextRow < 200 { + if diffIndex != LeftIndex { + panic(fmt.Sprintf("expected LeftIndex, got %d", diffIndex)) + } + newRow = 0 + newCol = nextRow - 100 + if getBoxNumber(newRow, newCol) != 2 { + panic(fmt.Sprintf("expected to move to box 2, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, DownIndex + } + + // 1 -> 6 + if getBoxNumber(r, c) == 1 && + nextRow == -1 && 100 <= nextCol && nextCol < 150 { + if diffIndex != UpIndex { + panic(fmt.Sprintf("expected UpIndex, got %d", diffIndex)) + } + newRow = 199 + newCol = nextCol - 100 + if getBoxNumber(newRow, newCol) != 6 { + panic(fmt.Sprintf("expected to move to box 6, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, UpIndex + } + // 6 -> 1 + if getBoxNumber(r, c) == 6 && + nextRow == 200 && 0 <= nextCol && nextCol < 50 { + if diffIndex != DownIndex { + panic(fmt.Sprintf("expected DownIndex, got %d", diffIndex)) + } + newRow = 0 + newCol = nextCol + 100 + if getBoxNumber(newRow, newCol) != 1 { + panic(fmt.Sprintf("expected to move to box 1, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, DownIndex + } + + // 4 -> 6 + if getBoxNumber(r, c) == 4 && + nextRow == 150 && 50 <= nextCol && nextCol < 100 { + if diffIndex != DownIndex { + panic(fmt.Sprintf("expected DownIndex, got %d", diffIndex)) + } + newRow = nextCol + 100 + newCol = 49 + if getBoxNumber(newRow, newCol) != 6 { + panic(fmt.Sprintf("expected to move to box 6, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, LeftIndex + } + // 6 -> 4 + if getBoxNumber(r, c) == 6 && + nextCol == 50 && 150 <= nextRow && nextRow < 200 { + if diffIndex != RightIndex { + panic(fmt.Sprintf("expected RightIndex, got %d", diffIndex)) + } + newRow = 149 + newCol = nextRow - 100 + if getBoxNumber(newRow, newCol) != 4 { + panic(fmt.Sprintf("expected to move to box 4, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, UpIndex + } + + // 4 -> 1 + if getBoxNumber(r, c) == 4 && + nextCol == 100 && 100 <= nextRow && nextRow < 150 { + if diffIndex != RightIndex { + panic(fmt.Sprintf("expected RightIndex, got %d", diffIndex)) + } + newRow = 149 - nextRow + newCol = 149 + if getBoxNumber(newRow, newCol) != 1 { + panic(fmt.Sprintf("expected to move to box 1, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, LeftIndex + } + // 1 -> 4 + if getBoxNumber(r, c) == 1 && + nextCol == 150 && 0 <= nextRow && nextRow < 50 { + if diffIndex != RightIndex { + panic(fmt.Sprintf("expected RightIndex, got %d", diffIndex)) + } + newRow = 149 - nextRow + newCol = 99 + if getBoxNumber(newRow, newCol) != 4 { + panic(fmt.Sprintf("expected to move to box 4, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, LeftIndex + } + + // 3 -> 1 + if getBoxNumber(r, c) == 3 && + nextCol == 100 && 50 <= nextRow && nextRow < 100 { + if diffIndex != RightIndex { + panic(fmt.Sprintf("expected RightIndex, got %d", diffIndex)) + } + newRow = 49 + newCol = nextRow + 50 + if getBoxNumber(newRow, newCol) != 1 { + panic(fmt.Sprintf("expected to move to box 1, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, UpIndex + } + // 1 -> 3 + if getBoxNumber(r, c) == 1 && + nextRow == 50 && 100 <= nextCol && nextCol < 150 { + if diffIndex != DownIndex { + panic(fmt.Sprintf("expected DownIndex, got %d", diffIndex)) + } + newRow = nextCol - 50 + newCol = 99 + if getBoxNumber(newRow, newCol) != 3 { + panic(fmt.Sprintf("expected to move to box 3, got %d", getBoxNumber(newRow, newCol))) + } + return newRow, newCol, LeftIndex + } + + // no edge conversion required, just pass through + return nextRow, nextCol, diffIndex +} + +func getBoxNumber(r, c int) int { + if 0 <= r && r < 50 && 100 <= c && c < 150 { + return 1 + } + if 0 <= r && r < 50 && 50 <= c && c < 100 { + return 2 + } + if 50 <= r && r < 100 && 50 <= c && c < 100 { + return 3 + } + if 100 <= r && r < 150 && 50 <= c && c < 100 { + return 4 + } + if 100 <= r && r < 150 && 0 <= c && c < 50 { + return 5 + } + if 150 <= r && r < 200 && 0 <= c && c < 50 { + return 6 + } + + panic(fmt.Sprintf("bad row %d and col %d", r, c)) +} diff --git a/2022/day22/main_test.go b/2022/day22/main_test.go new file mode 100644 index 0000000..156a825 --- /dev/null +++ b/2022/day22/main_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "testing" +) + +var example = ` ...# + .#.. + #... + .... +...#.......# +........#... +..#....#.... +..........#. + ...#.... + .....#.. + .#...... + ......#. + +10R5L5R10L4R5L5` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 6032, + }, + { + name: "actual", + input: input, + want: 144244, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + // NOTE: did not account for the example case because I manually coded + // for the shape of my exact input... which differed from the + // input shape + // { + // name: "example", + // input: example, + // want: 5031, + // }, + { + name: "actual", + input: input, + want: 138131, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day23/main.go b/2022/day23/main.go new file mode 100644 index 0000000..a8225ed --- /dev/null +++ b/2022/day23/main.go @@ -0,0 +1,207 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "math" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + ans := unstableDiffusion(input, part) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) +} + +var diffsSlice = [][][2]int{ + // N + { + {-1, -1}, + {-1, 0}, + {-1, 1}, + }, + // S + { + {1, -1}, + {1, 0}, + {1, 1}, + }, + // W + { + {-1, -1}, + {0, -1}, + {1, -1}, + }, + // E + { + {-1, 1}, + {0, 1}, + {1, 1}, + }, +} +var targetDiff = [][2]int{ + {-1, 0}, // N + {1, 0}, // S + {0, -1}, // W + {0, 1}, // E +} + +func unstableDiffusion(input string, part int) int { + elfCoords := parseInput(input) + diffStartIndex := 0 + + // part 2 + var lastState string + + round := 1 + + for { + if part == 1 && round == 11 { + break + } + + elfPlannedMoves := [][][2]int{} + elvesTargetingCoord := map[[2]int]int{} + + for coords, val := range elfCoords { + if val == "#" { + nonZeroNeighbors := 0 + for _, diffSlice := range diffsSlice { + for _, d := range diffSlice { + if elfCoords[[2]int{ + coords[0] + d[0], + coords[1] + d[1], + }] == "#" { + nonZeroNeighbors++ + } + } + } + + if nonZeroNeighbors == 0 { + elfPlannedMoves = append(elfPlannedMoves, [][2]int{ + coords, coords, + }) + elvesTargetingCoord[coords]++ + } else { + foundAMove := false + for i := 0; i < 4; i++ { + diffSliceIndex := i + diffStartIndex + diffSlice := diffsSlice[diffSliceIndex%4] + neighbors := 0 + for _, d := range diffSlice { + if elfCoords[[2]int{ + coords[0] + d[0], + coords[1] + d[1], + }] == "#" { + neighbors++ + } + } + if neighbors == 0 { + nextCoords := coords + nextCoords[0] += targetDiff[diffSliceIndex%4][0] + nextCoords[1] += targetDiff[diffSliceIndex%4][1] + + elfPlannedMoves = append(elfPlannedMoves, [][2]int{ + coords, nextCoords, + }) + elvesTargetingCoord[nextCoords]++ + + foundAMove = true + break + } + } + if !foundAMove { + elfPlannedMoves = append(elfPlannedMoves, [][2]int{ + coords, coords, + }) + elvesTargetingCoord[coords]++ + } + } + + } + } + + // reset coords, but only if elves are not blocked... + elfCoords = map[[2]int]string{} + + for _, plannedMove := range elfPlannedMoves { + if elvesTargetingCoord[plannedMove[1]] > 1 { + // stay + elfCoords[plannedMove[0]] = "#" + } else { + // move + elfCoords[plannedMove[1]] = "#" + } + } + + // rotate directions that are checked + diffStartIndex++ + + if part == 2 { // hash the state + allCoords := []string{} + for c := range elfCoords { + allCoords = append(allCoords, fmt.Sprint(c)) + } + sort.Strings(allCoords) + thisState := fmt.Sprint(allCoords) + if lastState == thisState { + return round + } + lastState = thisState + } + + round++ + } + + lowRow, highRow, lowCol, highCol := math.MaxInt16, math.MinInt16, math.MaxInt16, math.MinInt16 + for coords := range elfCoords { + lowRow = mathy.MinInt(lowRow, coords[0]) + highRow = mathy.MaxInt(highRow, coords[0]) + lowCol = mathy.MinInt(lowCol, coords[1]) + highCol = mathy.MaxInt(highCol, coords[1]) + } + + ans := 0 + for r := lowRow; r <= highRow; r++ { + for c := lowCol; c <= highCol; c++ { + if elfCoords[[2]int{r, c}] != "#" { + ans++ + } + } + } + + return ans +} + +func parseInput(input string) map[[2]int]string { + ans := map[[2]int]string{} + for r, line := range strings.Split(input, "\n") { + for c, v := range strings.Split(line, "") { + if v == "#" { + ans[[2]int{r, c}] = "#" + } + } + } + return ans +} diff --git a/2022/day23/main_test.go b/2022/day23/main_test.go new file mode 100644 index 0000000..095162b --- /dev/null +++ b/2022/day23/main_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "testing" +) + +var example = `....#.. +..###.# +#...#.# +.#...## +#.###.. +##.#.## +.#..#..` + +func Test_unstableDiffusion(t *testing.T) { + tests := []struct { + name string + input string + part int + want int + }{ + { + name: "small example", + input: `..... +..##. +..#.. +..... +..##. +.....`, + part: 1, + want: 30 - 5, + }, + { + name: "example", + input: example, + part: 1, + want: 110, + }, + { + name: "actual", + input: input, + part: 1, + want: 4116, + }, + + // + { + name: "example", + input: example, + part: 2, + want: 20, + }, + { + name: "actual", + input: input, + part: 2, + want: 984, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := unstableDiffusion(tt.input, tt.part); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day24/main.go b/2022/day24/main.go new file mode 100644 index 0000000..c251bf0 --- /dev/null +++ b/2022/day24/main.go @@ -0,0 +1,286 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := blizzardJourney(input, 1) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := blizzardJourney(input, 2) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func blizzardJourney(input string, part int) int { + start, end, blizzards, totalRows, totalCols := parseInput(input) + + // some form of BFS seems to be the obvious answer for "least steps" + // + // if maps are used for all blizzard coords... then the map will have to be + // constantly copied for new states + // want something... less involved + // + // could represent all blizzards fairly easily via maths + // < and > stay in same row, col = (starting +/- time) % (# of cols) + // then it's just math for "is there a blizzard here" but that does require + // iterating through every blizzard, but that's a known amount so maybe that's fine... aka close enough to constant + // + // or just calculate state at time t and store that matrix of "occupied" spaces, and then no need to recalculate + // per blizzard per time + + stepsForFirstLeg := bfs(blizzards, start, end, totalRows, totalCols, 0) + if part == 1 { + return stepsForFirstLeg + } + stepsBackToStart := bfs(blizzards, end, start, totalRows, totalCols, stepsForFirstLeg) + return bfs(blizzards, start, end, totalRows, totalCols, stepsBackToStart) +} + +func bfs(blizzards []blizzard, start, end [2]int, totalRows, totalCols, stepsElapsedAlready int) int { + cacheRoomStates := map[int][][]string{} + + type node struct { + coords [2]int + steps int + debugPath string + } + + queue := []node{} + queue = append(queue, node{ + coords: start, + steps: stepsElapsedAlready, + debugPath: fmt.Sprint(0, start), + }) + + seenCoordsSteps := map[[3]int]bool{} + for len(queue) > 0 { + popped := queue[0] + queue = queue[1:] + + roomState := calcOrGetRoomState(blizzards, popped.steps+1, totalRows, totalCols, cacheRoomStates) + + for _, diff := range [][2]int{ + {1, 0}, + {0, 1}, + {0, -1}, + {-1, 0}, + } { + nextCoords := [2]int{ + popped.coords[0] + diff[0], + popped.coords[1] + diff[1], + } + + if nextCoords == start { + continue + } + if nextCoords != start && nextCoords != end { + if nextCoords[0] < 0 || nextCoords[0] >= totalRows || + nextCoords[1] < 0 || nextCoords[1] >= totalCols { + continue + } + } + + // no point in processing a coordinate & steps pair that has already been seen + hash := [3]int{nextCoords[0], nextCoords[1], popped.steps + 1} + if seenCoordsSteps[hash] { + continue + } + seenCoordsSteps[hash] = true + + // because of how i indexed the room, need to do literal checks to see if we're in start + // or end coords + + // if blocked, continue + if nextCoords != start && nextCoords != end && + roomState[nextCoords[0]][nextCoords[1]] != "." { + continue + } + + // if out of bounds, continue + if nextCoords != start && nextCoords != end { + if nextCoords[0] < 0 || nextCoords[0] >= totalRows || + nextCoords[1] < 0 || nextCoords[1] >= totalCols { + continue + } + } + + // done + if nextCoords == end { + return popped.steps + 1 + } + + queue = append(queue, node{ + coords: nextCoords, + steps: popped.steps + 1, + debugPath: popped.debugPath + fmt.Sprint(popped.steps+1, nextCoords), + }) + } + // if possible to stay still, add "wait" move + if popped.coords == start || + roomState[popped.coords[0]][popped.coords[1]] == "." { + queue = append(queue, node{ + coords: popped.coords, + steps: popped.steps + 1, + debugPath: popped.debugPath + fmt.Sprint(popped.steps+1, popped.coords), + }) + } + } + + panic("should return from loop") +} + +type blizzard struct { + startRow, startCol int + rowSlope, colSlope int + totalRows, totalCols int + char string +} + +func (b blizzard) calculateCoords(steps int) [2]int { + row := (b.startRow + b.rowSlope*steps) % b.totalRows + col := (b.startCol + b.colSlope*steps) % b.totalCols + + row += b.totalRows + col += b.totalCols + row %= b.totalRows + col %= b.totalCols + + return [2]int{ + row, col, + } +} + +// occupied coordinates are easy to calculate based on each blizzard's movement +// and steps/time elapsed, return a matrix that represents occupied cells +// and store the result in a map to reduce future calcs +func calcOrGetRoomState(blizzards []blizzard, steps, totalRows, totalCols int, memo map[int][][]string) [][]string { + if m, ok := memo[steps]; ok { + return m + } + + matrix := make([][]string, totalRows) + for r := range matrix { + matrix[r] = make([]string, totalCols) + } + + for _, b := range blizzards { + coords := b.calculateCoords(steps) + matrix[coords[0]][coords[1]] = b.char + } + for r := 0; r < len(matrix); r++ { + for c := 0; c < len(matrix[0]); c++ { + if matrix[r][c] == "" { + matrix[r][c] = "." + } + } + } + + memo[steps] = matrix + + return matrix +} + +func parseInput(input string) ([2]int, [2]int, []blizzard, int, int) { + var start, end [2]int + blizzards := []blizzard{} + + lines := strings.Split(input, "\n") + + for c := 0; c < len(lines); c++ { + if lines[0][c] == '.' { + start = [2]int{-1, c - 1} + break + } + } + + // 0,0 will be within the BOX we start in + // start and end will be off the bounds of that box + totalRows := len(lines) - 2 + totalCols := len(lines[0]) - 2 + + for c := 0; c < len(lines[0]); c++ { + if lines[len(lines)-1][c] == '.' { + end = [2]int{totalRows, c - 1} + break + } + } + + for l := 1; l < len(lines)-1; l++ { + chars := strings.Split(lines[l], "") + for c := 1; c < len(chars)-1; c++ { + switch chars[c] { + case ">": + blizzards = append(blizzards, blizzard{ + startRow: l - 1, + startCol: c - 1, + rowSlope: 0, + colSlope: 1, + totalRows: totalRows, + totalCols: totalCols, + char: ">", + }) + case "<": + blizzards = append(blizzards, blizzard{ + startRow: l - 1, + startCol: c - 1, + rowSlope: 0, + colSlope: -1, + totalRows: totalRows, + totalCols: totalCols, + char: "<", + }) + case "^": + blizzards = append(blizzards, blizzard{ + startRow: l - 1, + startCol: c - 1, + rowSlope: -1, + colSlope: 0, + totalRows: totalRows, + totalCols: totalCols, + char: "^", + }) + case "v": + blizzards = append(blizzards, blizzard{ + startRow: l - 1, + startCol: c - 1, + rowSlope: 1, + colSlope: 0, + totalRows: totalRows, + totalCols: totalCols, + char: "v", + }) + case ".", "#": + default: + panic("unhandled char") + } + } + } + + return start, end, blizzards, totalRows, totalCols +} diff --git a/2022/day24/main_test.go b/2022/day24/main_test.go new file mode 100644 index 0000000..ef6ab8d --- /dev/null +++ b/2022/day24/main_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "testing" +) + +var example = `#.###### +#>>.<^<# +#.<..<<# +#>v.><># +#<^v^^># +######.#` + +func Test_blizzardJourney(t *testing.T) { + tests := []struct { + name string + input string + part int + want int + }{ + { + name: "example", + input: example, + part: 1, + want: 18, + }, + { + name: "actual", + input: input, + part: 1, + want: 240, + }, + { + name: "example", + input: example, + part: 2, + want: 54, + }, + { + name: "actual", + input: input, + part: 2, + want: 717, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := blizzardJourney(tt.input, tt.part); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2022/day25/main.go b/2022/day25/main.go new file mode 100644 index 0000000..2a89d68 --- /dev/null +++ b/2022/day25/main.go @@ -0,0 +1,117 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) string { + snafuNums := strings.Split(input, "\n") + // sum of the fuel requirements + // power of 5... + // 2 is 2 + // 1 is 1 + // 0 is 0 + // -1 is - + // -2 is = + sum := "" + for _, n := range snafuNums { + sum = addSnafu(sum, n) + } + + return sum +} + +func addSnafu(one, two string) string { + // reversed... + split1, split2 := strings.Split(one, ""), strings.Split(two, "") + var reversed1, reversed2 []string + for i := len(split1) - 1; i >= 0; i-- { + reversed1 = append(reversed1, split1[i]) + } + for i := len(split2) - 1; i >= 0; i-- { + reversed2 = append(reversed2, split2[i]) + } + + longer, shorter := reversed1, reversed2 + if len(longer) < len(shorter) { + longer, shorter = shorter, longer + } + + charToVal := map[string]int{ + "=": -2, + "-": -1, + "0": 0, + "1": 1, + "2": 2, + } + valToChar := map[int]string{ + -2: "=", + -1: "-", + 0: "0", + 1: "1", + 2: "2", + } + + ans := make([]int, len(longer)+1) + for i := 0; i < len(longer); i++ { + sum := charToVal[longer[i]] + if i < len(shorter) { + sum += charToVal[shorter[i]] + } + ans[i] += sum + if ans[i] > 2 { + ans[i] -= 5 + ans[i+1]++ + } else if ans[i] < -2 { + ans[i] += 5 + ans[i+1]-- + } + } + + for ans[len(ans)-1] == 0 { + ans = ans[:len(ans)-1] + } + + snafu := "" + for _, a := range ans { + snafu = valToChar[a] + snafu + } + return snafu +} + +func part2(input string) string { + return ":)" +} diff --git a/2022/day25/main_test.go b/2022/day25/main_test.go new file mode 100644 index 0000000..ffdd166 --- /dev/null +++ b/2022/day25/main_test.go @@ -0,0 +1,67 @@ +package main + +import ( + _ "embed" + "testing" +) + +var example = `1=-0-2 +12111 +2=0= +21 +2=01 +111 +20012 +112 +1=-1= +1-12 +12 +1= +122` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "example", + input: example, + want: "2=-1=0", + }, + { + name: "actual", + input: input, + want: "2----0=--1122=0=0021", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "example", + input: example, + want: ":)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day01/main.go b/2023/day01/main.go new file mode 100644 index 0000000..2ec0de6 --- /dev/null +++ b/2023/day01/main.go @@ -0,0 +1,110 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + var sum int + for _, line := range strings.Split(input, "\n") { + var tens, ones int + for i := 0; i < len(line); i++ { + if strings.ContainsAny(line[i:i+1], "0123456789") { + tens = cast.ToInt(line[i : i+1]) + break + } + } + for i := len(line) - 1; i >= 0; i-- { + if strings.ContainsAny(line[i:i+1], "0123456789") { + ones = cast.ToInt(line[i : i+1]) + break + } + } + sum += tens*10 + ones + } + + return sum +} + +func part2(input string) int { + prefixes := map[string]int{ + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + "zero": 0, + } + for i := 0; i <= 9; i++ { + prefixes[cast.ToString(i)] = i + } + + var sum int + for _, line := range strings.Split(input, "\n") { + var first, last int + + for len(line) > 0 { + for prefix, val := range prefixes { + if doesStringHavePrefix(line, prefix) { + if first == 0 { + first = val + } + last = val + break + } + } + + // shorten line + line = line[1:] + } + + sum += first*10 + last + } + + return sum +} + +func doesStringHavePrefix(str string, prefix string) bool { + if len(str) < len(prefix) { + return false + } + return str[:len(prefix)] == prefix +} diff --git a/2023/day01/main_test.go b/2023/day01/main_test.go new file mode 100644 index 0000000..9b773de --- /dev/null +++ b/2023/day01/main_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "testing" +) + +var example = `1abc2 +pqr3stu8vwx +a1b2c3d4e5f +treb7uchet` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 142, + }, + { + name: "actual", + input: input, + want: 55488, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +var example2 = `two1nine +eightwothree +abcone2threexyz +xtwone3four +4nineeightseven2 +zoneight234 +7pqrstsixteen` + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example2, + want: 281, + }, + { + name: "actual", + input: input, + want: 55614, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day02/main.go b/2023/day02/main.go new file mode 100644 index 0000000..0fcb6f7 --- /dev/null +++ b/2023/day02/main.go @@ -0,0 +1,112 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + games := parseInput(input) + + var possibleIDSum int + + for _, g := range games { + isPossible := true + for _, step := range g.steps { + // only 12 red cubes, 13 green cubes, and 14 blue cubes + if step["red"] > 12 || step["green"] > 13 || step["blue"] > 14 { + isPossible = false + break + } + } + + if isPossible { + possibleIDSum += g.id + } + } + + return possibleIDSum +} + +func part2(input string) int { + games := parseInput(input) + + var sum int + + for _, g := range games { + lowestPossibleCount := map[string]int{} + + for _, step := range g.steps { + lowestPossibleCount["red"] = mathy.MaxInt(lowestPossibleCount["red"], step["red"]) + lowestPossibleCount["green"] = mathy.MaxInt(lowestPossibleCount["green"], step["green"]) + lowestPossibleCount["blue"] = mathy.MaxInt(lowestPossibleCount["blue"], step["blue"]) + } + + sum += lowestPossibleCount["red"] * lowestPossibleCount["blue"] * lowestPossibleCount["green"] + } + + return sum +} + +type game struct { + id int + steps []map[string]int +} + +func parseInput(input string) (ans []game) { + for i, line := range strings.Split(input, "\n") { + parts := strings.Split(line, ": ") + g := game{ + id: i + 1, + } + + for _, p := range strings.Split(parts[1], "; ") { + step := map[string]int{} + for _, group := range strings.Split(p, ", ") { + numberColor := strings.Split(group, " ") + if len(numberColor) != 2 { + panic(fmt.Sprintf("group not in two pieces %q", group)) + } + + step[numberColor[1]] = cast.ToInt(numberColor[0]) + } + g.steps = append(g.steps, step) + } + ans = append(ans, g) + } + return ans +} diff --git a/2023/day02/main_test.go b/2023/day02/main_test.go new file mode 100644 index 0000000..dcf2c1c --- /dev/null +++ b/2023/day02/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "testing" +) + +var example = `Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green +Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue +Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red +Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red +Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 8, + }, + { + name: "actual", + input: input, + want: 2632, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 2286, + }, + // { + // name: "actual", + // input: input, + // want: 0, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day03/main.go b/2023/day03/main.go new file mode 100644 index 0000000..d24e0b2 --- /dev/null +++ b/2023/day03/main.go @@ -0,0 +1,198 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "regexp" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +var numReg = regexp.MustCompile("[0-9]") + +func part1(input string) int { + matrix := [][]string{} + + // collect special characters + specialChars := map[string]bool{} + for _, row := range strings.Split(input, "\n") { + matrix = append(matrix, strings.Split(row, "")) + for _, val := range strings.Split(row, "") { + if !numReg.MatchString(val) && val != "." { + specialChars[val] = true + } + } + } + specialCharsString := "" + for k := range specialChars { + specialCharsString += k + } + + var sum int + + diffs := [8][2]int{ + {-1, -1}, + {-1, 0}, + {-1, 1}, + {0, -1}, + {0, 1}, + {1, -1}, + {1, 0}, + {1, 1}, + } + + seen := map[[2]int]bool{} + for r, row := range matrix { + for c, val := range row { + coords := [2]int{r, c} + if seen[coords] { + continue + } + seen[coords] = true + + // if we hit a number, collect the entire number and check along the way if it's + // adjacent to a special char + if numReg.MatchString(val) { + hasAdjacentSpecialChar := false + + numStr := "" + for j := 0; j+c < len(matrix[0]); j++ { + char := row[c+j] + // breaks on period or special character, loop itself breaks on out of range + if !numReg.MatchString(char) { + break + } + // keep collecting number + numStr += char + + // check all 8 directions for special char + for _, d := range diffs { + dr, dc := r+d[0], c+j+d[1] + if dr >= 0 && dr < len(matrix) && dc >= 0 && dc < len(matrix[0]) { + if strings.ContainsAny(matrix[dr][dc], specialCharsString) { + hasAdjacentSpecialChar = true + } + seen[[2]int{r, c + j}] = true + } + } + } + + if hasAdjacentSpecialChar { + sum += cast.ToInt(numStr) + } + + } + } + } + + return sum +} + +// getNumber returns -1 if a number is "not found" which could include the number +// already being seen +func getNumber(matrix [][]string, coord [2]int, seen map[[2]int]bool) int { + if !numReg.MatchString(matrix[coord[0]][coord[1]]) { + return -1 + } + if seen[coord] { + return -1 + } + // go to the left most digit + r, c := coord[0], coord[1] + for c-1 >= 0 { + if numReg.MatchString(matrix[r][c-1]) { + c-- + } else { + break + } + } + + numStr := "" + + for c < len(matrix[0]) && numReg.MatchString(matrix[r][c]) { + numStr += matrix[r][c] + seen[[2]int{r, c}] = true + c++ + } + + return cast.ToInt(numStr) +} + +func part2(input string) int { + // lucky edge case that there are not multiple gears that share the same number such as + // ....*.... + // .123.456. OR ...123*456*789... + // ....*.... + // with the current useage of "seen", this would only get counted once + seen := map[[2]int]bool{} + + matrix := [][]string{} + for _, row := range strings.Split(input, "\n") { + matrix = append(matrix, strings.Split(row, "")) + } + + diffs := [8][2]int{ + {-1, -1}, + {-1, 0}, + {-1, 1}, + {0, -1}, + {0, 1}, + {1, -1}, + {1, 0}, + {1, 1}, + } + + sum := 0 + for r, rows := range matrix { + for c, val := range rows { + if val == "*" { + nums := []int{} + for _, diff := range diffs { + dr, dc := r+diff[0], c+diff[1] + if dr >= 0 && dr < len(matrix) && dc >= 0 && dc < len(matrix[0]) { + foundNum := getNumber(matrix, [2]int{dr, dc}, seen) + if foundNum != -1 { + nums = append(nums, foundNum) + } + } + } + + if len(nums) == 2 { + sum += nums[0] * nums[1] + } + } + } + } + return sum +} diff --git a/2023/day03/main_test.go b/2023/day03/main_test.go new file mode 100644 index 0000000..1aed0f5 --- /dev/null +++ b/2023/day03/main_test.go @@ -0,0 +1,68 @@ +package main + +import ( + "testing" +) + +var example = `467..114.. +...*...... +..35..633. +......#... +617*...... +.....+.58. +..592..... +......755. +...$.*.... +.664.598..` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 4361, + }, + { + name: "actual", + input: input, + want: 536576, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 467835, + }, + // { + // name: "actual", + // input: input, + // want: 0, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day04/main.go b/2023/day04/main.go new file mode 100644 index 0000000..3e1400c --- /dev/null +++ b/2023/day04/main.go @@ -0,0 +1,138 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + parsed := parseInput(input) + + var ans int + + for _, card := range parsed { + ans += scoreCard(card) + } + + return ans +} + +func scoreCard(c card) int { + var ans int + + for _, num := range c.hand { + if c.winning[num] { + if ans == 0 { + ans = 1 + } else { + ans *= 2 + } + } + } + + return ans +} + +func part2(input string) int { + cards := parseInput(input) + + // tracks the number of cards won, starts with 1 of each card + numCards := make([]int, len(cards)) + for i := range numCards { + numCards[i] = 1 + } + + for index, c := range cards { + cardsWon := countWinningNumbers(c) + for i := 1; i <= cardsWon; i++ { + // add number of current card to account for previous wins + numCards[index+i] += numCards[index] + } + } + + // add up total number of cards + var cardCount int + for _, n := range numCards { + cardCount += n + } + return cardCount +} + +func countWinningNumbers(c card) int { + var ans int + + for _, num := range c.hand { + if c.winning[num] { + ans++ + } + } + + return ans +} + +type card struct { + // index int // unused + winning map[int]bool + hand []int +} + +func parseInput(input string) (ans []card) { + for _, line := range strings.Split(input, "\n") { + c := card{ + // index: len(ans) + 1, + winning: map[int]bool{}, + } + + half := strings.Split(line, ": ") + numParts := strings.Split(half[1], " | ") + for _, winningNum := range strings.Split(numParts[0], " ") { + // handles single digits that have an extra empty string between nums + if winningNum == "" { + continue + } + c.winning[cast.ToInt(winningNum)] = true + } + + for _, handNum := range strings.Split(numParts[1], " ") { + if handNum == "" { + continue + } + c.hand = append(c.hand, cast.ToInt(handNum)) + } + ans = append(ans, c) + } + return ans +} diff --git a/2023/day04/main_test.go b/2023/day04/main_test.go new file mode 100644 index 0000000..053e861 --- /dev/null +++ b/2023/day04/main_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "testing" +) + +var example = `Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53 +Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19 +Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1 +Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83 +Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36 +Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 13, + }, + { + name: "actual", + input: input, + want: 19135, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 30, + }, + { + name: "actual", + input: input, + want: 5704953, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day05/main.go b/2023/day05/main.go new file mode 100644 index 0000000..f6217f0 --- /dev/null +++ b/2023/day05/main.go @@ -0,0 +1,196 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "math" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + seeds, listOfMappingRanges := parseInput(input) + + lowestFinalNum := math.MaxInt64 + + for _, seed := range seeds { + finalMapping := getFinalMapping(seed, listOfMappingRanges) + lowestFinalNum = mathy.MinInt(lowestFinalNum, finalMapping) + } + + return lowestFinalNum +} + +func getFinalMapping(seed int, listOfMappingRanges [][]mappingRange) int { + currentVal := seed + + // jeez these are horrible variable names... + for _, mappingRanges := range listOfMappingRanges { + // if currentVal is between the sourceStart and sourceStart plus count, a mappingRange is found + // otherwise it maps to the same value and move on + // [sourceStart, sourceStart + count) <- not inclusive of sourceStart + count + for _, m := range mappingRanges { + if m.sourceStart <= currentVal && currentVal < m.sourceStart+m.count { + currentVal = m.destinationStart + (currentVal - m.sourceStart) + // then break so number is only mapped once per round of mappings + break + } + } + } + + return currentVal +} + +type mappingRange struct { + // seed to soil map: 50 98 2 + // means 2 mappings total: seed 98 maps to water 50 and seed 99 maps to soil 51 + // if a mappingRange does not exist, then it maps to the same number + sourceStart, destinationStart, count int +} + +func parseInput(input string) (seeds []int, listOfMappingRanges [][]mappingRange) { + parts := strings.Split(input, "\n\n") + + seedParts := strings.Split(parts[0], " ") + // skip first part which is "seeds: " + for i := 1; i < len(seedParts); i++ { + seeds = append(seeds, cast.ToInt(seedParts[i])) + } + + // parse mappings + for p := 1; p < len(parts); p++ { + // get separate lines and ignore throw away first line of text + mappingLines := strings.Split(parts[p], "\n")[1:] + var mappings []mappingRange + for _, l := range mappingLines { + lineParts := strings.Split(l, " ") + mappings = append(mappings, mappingRange{ + destinationStart: cast.ToInt(lineParts[0]), + sourceStart: cast.ToInt(lineParts[1]), + count: cast.ToInt(lineParts[2]), + }) + } + + listOfMappingRanges = append(listOfMappingRanges, mappings) + } + + return seeds, listOfMappingRanges +} + +func part2(input string) int { + seedRanges, listOfMappingRanges := parseInput(input) + + lowestFinalNum := math.MaxInt64 + + // store final mappings to save on duplicate calcs? + // not sure if that helped. brute force solution worked in a minute or two + // finalMappings := map[int]int{} + + for i := 0; i < len(seedRanges); i += 2 { + for count := 0; count < seedRanges[i+1]; count++ { + seed := seedRanges[i] + count + finalMapping := getFinalMapping(seed, listOfMappingRanges) + lowestFinalNum = mathy.MinInt(lowestFinalNum, finalMapping) + } + + // progress check... + fmt.Println(i, len(seedRanges)) + } + + return lowestFinalNum +} + +// //////////// +// naive solutions that are too slow with actual input +// + +func naivePart1(input string) int { + seeds, allMaps := naiveParseInput(input) + lowestFinalNum := math.MaxInt64 + + for _, s := range seeds { + finalLocation := naiveMapSeedToFinalLocation(s, allMaps) + lowestFinalNum = mathy.MinInt(lowestFinalNum, finalLocation) + } + + return lowestFinalNum +} + +func naiveMapSeedToFinalLocation(seed int, allMaps []map[int]int) int { + mappedNum := seed + + for _, m := range allMaps { + num, ok := m[mappedNum] + if ok { + mappedNum = num + } + } + + return mappedNum +} + +// assume that the mappings are in a logical order in the input +// seeds -> soil -> fertilizer -> water -> etc and not out of order... +// so a slice works fine for storing the mappings +func naiveParseInput(input string) (seeds []int, allMaps []map[int]int) { + // seed to soil map: 50 98 2 + // means 2 mappings total: seed 98 maps to water 50 and seed 99 maps to soil 51 + // if a mappingRange does not exist, then it maps to the same number + + parts := strings.Split(input, "\n\n") + + seedParts := strings.Split(parts[0], " ") + // skip first part which is "seeds: " + for i := 1; i < len(seedParts); i++ { + seeds = append(seeds, cast.ToInt(seedParts[i])) + } + + // mappings + for p := 1; p < len(parts); p++ { + // get separate lines and ignore throw away first line of text + mappingLines := strings.Split(parts[p], "\n")[1:] + m := map[int]int{} + for _, line := range mappingLines { + nums := strings.Split(line, " ") + destinationStart, sourceStart, count := cast.ToInt(nums[0]), cast.ToInt(nums[1]), cast.ToInt(nums[2]) + for c := 0; c < count; c++ { + m[sourceStart+c] = destinationStart + c + } + } + allMaps = append(allMaps, m) + } + + return seeds, allMaps +} diff --git a/2023/day05/main_test.go b/2023/day05/main_test.go new file mode 100644 index 0000000..2ee0cec --- /dev/null +++ b/2023/day05/main_test.go @@ -0,0 +1,91 @@ +package main + +import ( + "testing" +) + +var example = `seeds: 79 14 55 13 + +seed-to-soil map: +50 98 2 +52 50 48 + +soil-to-fertilizer map: +0 15 37 +37 52 2 +39 0 15 + +fertilizer-to-water map: +49 53 8 +0 11 42 +42 0 7 +57 7 4 + +water-to-light map: +88 18 7 +18 25 70 + +light-to-temperature map: +45 77 23 +81 45 19 +68 64 13 + +temperature-to-humidity map: +0 69 1 +1 0 69 + +humidity-to-location map: +60 56 37 +56 93 4` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 35, + }, + { + name: "actual", + input: input, + want: 88151870, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 46, + }, + { + name: "actual", + input: input, + want: 2008785, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day06/main.go b/2023/day06/main.go new file mode 100644 index 0000000..af38882 --- /dev/null +++ b/2023/day06/main.go @@ -0,0 +1,141 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "regexp" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + races := parseInputPart1(input) + ans := 1 + + for _, r := range races { + ans *= wayToWinRace(r) + } + + return ans +} + +func wayToWinRace(r race) int { + // probably fast enough to do this naively for part 1... + // Time: 40 92 97 90 + // total: 319 + // but could binary search this + + var ans int + for chargeTime := 1; chargeTime < r.time; chargeTime++ { + // velocity (chargeTime) * remaining time + dist := chargeTime * (r.time - chargeTime) + if dist > r.distance { + ans++ + } + } + + return ans +} + +func part2(input string) int { + combinedRace := parseInputPart2(input) + + // binary search this? + // but the left and right and middle could all fail if the distribution is + // F F P P P P F F F F F F F F F F F F + // F = Fail, P = Pass + // and it is some kind of (UN-CENTERED) bell curve distribution + // could test 1 by 1 from left and right of time bound... + + // unfortunately you can just brute force this. + return wayToWinRace(combinedRace) + + // all the optimizations might fall apart given unkind inputs, the example + // has a distribution with many passes in the middle: + // F P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P F + // but an unkind one could have F P F F F F F F F F F F F F F F F which could + // create a linear time complexity anyways... + + // maybe there's some kind of search where you divide the input times until + // you find a passing time, so in half, then quarters, then eights, etc + // once you find a passing time you have a reduced window to search + // but if the entire entry is 0 or 1 pass and the rest fails, then it + // degrades to linear in the worst case scenario anyways, so sanitized or + // expected inputs do go a long way +} + +type race struct { + time, distance int +} + +func parseInputPart1(input string) (ans []race) { + parts := strings.Split(input, "\n") + timeParts := strings.Split(parts[0], " ") + distParts := strings.Split(parts[1], " ") + + numRegexp := regexp.MustCompile("[0-9]+") + + var t, d int + for t < len(timeParts) && d < len(distParts) { + for !numRegexp.MatchString(timeParts[t]) { + t++ + } + for !numRegexp.MatchString(distParts[d]) { + d++ + } + ans = append(ans, race{ + time: cast.ToInt(timeParts[t]), + distance: cast.ToInt(distParts[d]), + }) + t++ + d++ + } + + return ans +} + +func parseInputPart2(input string) race { + parts := strings.Split(input, "\n") + timeLine := parts[0] + distLine := parts[1] + + nonNums := regexp.MustCompile("[^0-9]+") + timeLine = nonNums.ReplaceAllString(timeLine, "") + distLine = nonNums.ReplaceAllString(distLine, "") + + return race{ + time: cast.ToInt(timeLine), + distance: cast.ToInt(distLine), + } +} diff --git a/2023/day06/main_test.go b/2023/day06/main_test.go new file mode 100644 index 0000000..5216d4f --- /dev/null +++ b/2023/day06/main_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "testing" +) + +var example = `Time: 7 15 30 +Distance: 9 40 200` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 288, + }, + { + name: "actual", + input: input, + want: 6209190, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 71503, + }, + // { + // name: "actual", + // input: input, + // want: 0, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day07/main.go b/2023/day07/main.go new file mode 100644 index 0000000..a03be8f --- /dev/null +++ b/2023/day07/main.go @@ -0,0 +1,168 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) +} + +func part1(input string) int { + allPlayers := parseInput(input) + + sort.Sort(allPlayers) + + var ans int + for i, player := range allPlayers { + ans += int(player.bid) * int(i+1) + } + return ans +} + +type player struct { + hand string + bid int + + handTypeScore int +} + +func parseInput(input string) (ans SortablePlayers) { + for _, line := range strings.Split(input, "\n") { + handAndBid := strings.Split(line, " ") + pl := player{ + hand: handAndBid[0], + bid: cast.ToInt(handAndBid[1]), + } + pl.handTypeScore = scoreHandType(pl.hand) + ans = append(ans, pl) + } + return ans +} + +/* point assignments: + five of a kind -> 7 + four of a kind -> 6 + full house -> 5 + three of a kind -> 4 + two pair -> 3 + one pair -> 2 + high card -> 1 +*/ + +func scoreHandType(hand string) int { + counts := map[string]int{} + for _, card := range strings.Split(hand, "") { + counts[card]++ + } + + // high card + if len(counts) == 5 { + return 1 + } + // one pair + if len(counts) == 4 { + return 2 + } + + if len(counts) == 3 { + // either two pair or three of a kind + for _, ct := range counts { + if ct == 3 { + return 4 // 3 of a kind, 3 1 1 + } + } + return 3 // two pair, 2 2 1 + } + + if len(counts) == 2 { + // full house (3 2) or four of a kind (4 1) + for _, ct := range counts { + if ct == 3 { + return 5 + } + } + return 6 + } + + if len(counts) == 1 { + return 7 + } + + panic(fmt.Sprintf("error scoring hand: %+v", hand)) +} + +type SortablePlayers []player + +func (ps SortablePlayers) Len() int { return len(ps) } +func (ps SortablePlayers) Swap(i, j int) { + ps[i], ps[j] = ps[j], ps[i] +} +func (ps SortablePlayers) Less(i, j int) bool { + iTypeScore := ps[i].handTypeScore + jTypeScore := ps[j].handTypeScore + + if iTypeScore == jTypeScore { + // higher score goes to end of ps slice + return !doesPlayer1WinTiebreak(ps[i], ps[j]) + } + return iTypeScore < jTypeScore +} + +var cardValuesMap = map[string]int{ + "A": 14, + "K": 13, + "Q": 12, + "J": 11, + "T": 10, + "9": 9, + "8": 8, + "7": 7, + "6": 6, + "5": 5, + "4": 4, + "3": 3, + "2": 2, +} + +// returns true if player1 wins the tie break +func doesPlayer1WinTiebreak(p1, p2 player) bool { + if p1.handTypeScore != p2.handTypeScore { + panic("p1 and p2 scores do not have the same level hand") + } + p1Cards := strings.Split(p1.hand, "") + p2Cards := strings.Split(p2.hand, "") + for i := 0; i < len(p1Cards); i++ { + if cardValuesMap[p1Cards[i]] > cardValuesMap[p2Cards[i]] { + return true + } else if cardValuesMap[p1Cards[i]] < cardValuesMap[p2Cards[i]] { + return false + } + } + panic("not expecting to have two matching hands") +} diff --git a/2023/day07/main_test.go b/2023/day07/main_test.go new file mode 100644 index 0000000..a597505 --- /dev/null +++ b/2023/day07/main_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "testing" +) + +var example = `32T3K 765 +T55J5 684 +KK677 28 +KTJJT 220 +QQQJA 483` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 6440, + }, + { + name: "actual", + input: input, + want: 248569531, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day07/part2/main.go b/2023/day07/part2/main.go new file mode 100644 index 0000000..1abd8aa --- /dev/null +++ b/2023/day07/part2/main.go @@ -0,0 +1,196 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +/** +This implementation is too tangled to cover part 1 and 2 because the sort +logic is encapsulated in the interface which cannot (nicely) be made aware +of which part is being run... +There are two main differences highlighted by "// NOTE: diff to part 1 here" +*/ + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) +} + +func part2(input string) int { + allPlayers := parseInput(input, 1) + + sort.Sort(allPlayers) + + var ans int + for i, player := range allPlayers { + ans += int(player.bid) * int(i+1) + } + return ans +} + +type player struct { + hand string + bid int + + handTypeScore int +} + +func parseInput(input string, part int) (ans SortablePlayers) { + for _, line := range strings.Split(input, "\n") { + handAndBid := strings.Split(line, " ") + pl := player{ + hand: handAndBid[0], + bid: cast.ToInt(handAndBid[1]), + } + pl.handTypeScore = scoreHandType(pl, part) + ans = append(ans, pl) + } + return ans +} + +/* point assignments: + five of a kind -> 7 + four of a kind -> 6 + full house -> 5 + three of a kind -> 4 + two pair -> 3 + one pair -> 2 + high card -> 1 +*/ + +func scoreHandType(p player, part int) int { + counts := map[string]int{} + for _, card := range strings.Split(p.hand, "") { + counts[card]++ + } + + // NOTE: diff to part 1 here + if counts["J"] != 0 { + // add the J counts to the highest card's count as that will always result in the best hand? + jCount := counts["J"] + // remove "J" from map in case if it's the highest count + delete(counts, "J") + + var highestCard string + var highestCount int + for card, ct := range counts { + if ct > highestCount { + highestCount = ct + highestCard = card + } + } + + counts[highestCard] += jCount + } + + // high card + if len(counts) == 5 { + return 1 + } + // one pair + if len(counts) == 4 { + return 2 + } + + if len(counts) == 3 { + // either two pair or three of a kind + for _, ct := range counts { + if ct == 3 { + return 4 // 3 of a kind, 3 1 1 + } + } + return 3 // two pair, 2 2 1 + } + + if len(counts) == 2 { + // full house (3 2) or four of a kind (4 1) + for _, ct := range counts { + if ct == 3 { + return 5 + } + } + return 6 + } + + if len(counts) == 1 { + return 7 + } + + panic(fmt.Sprintf("error scoring hand: %+v", p.hand)) +} + +type SortablePlayers []player + +func (ps SortablePlayers) Len() int { return len(ps) } +func (ps SortablePlayers) Swap(i, j int) { + ps[i], ps[j] = ps[j], ps[i] +} +func (ps SortablePlayers) Less(i, j int) bool { + iTypeScore := ps[i].handTypeScore + jTypeScore := ps[j].handTypeScore + + if iTypeScore == jTypeScore { + // higher score goes to end of ps slice + return !doesPlayer1WinTiebreak(ps[i], ps[j]) + } + return iTypeScore < jTypeScore +} + +var cardValuesMap = map[string]int{ + "A": 14, + "K": 13, + "Q": 12, + // can just leave the same values though, the heiarchy is what matters + "T": 10, + "9": 9, + "8": 8, + "7": 7, + "6": 6, + "5": 5, + "4": 4, + "3": 3, + "2": 2, + // NOTE: diff to part 1 here: J is the worst card now + "J": 0, +} + +// returns true if player1 wins the tie break +func doesPlayer1WinTiebreak(p1, p2 player) bool { + if p1.handTypeScore != p2.handTypeScore { + panic("p1 and p2 scores do not have the same level hand") + } + p1Cards := strings.Split(p1.hand, "") + p2Cards := strings.Split(p2.hand, "") + for i := 0; i < len(p1Cards); i++ { + if cardValuesMap[p1Cards[i]] > cardValuesMap[p2Cards[i]] { + return true + } else if cardValuesMap[p1Cards[i]] < cardValuesMap[p2Cards[i]] { + return false + } + } + panic("not expecting to have two matching hands") +} diff --git a/2023/day07/part2/main_test.go b/2023/day07/part2/main_test.go new file mode 100644 index 0000000..2c071ff --- /dev/null +++ b/2023/day07/part2/main_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "testing" +) + +var example = `32T3K 765 +T55J5 684 +KK677 28 +KTJJT 220 +QQQJA 483` + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 5905, + }, + { + name: "actual", + input: input, + want: 250382098, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day08/main.go b/2023/day08/main.go new file mode 100644 index 0000000..7b69902 --- /dev/null +++ b/2023/day08/main.go @@ -0,0 +1,145 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + steps, graph := parseInput(input) + + location := "AAA" + numOfSteps := 0 + for location != "ZZZ" { + dir := steps[numOfSteps%len(steps)] + + if dir == "L" { + location = graph[location][0] + } else { + location = graph[location][1] + } + + numOfSteps++ + } + + return numOfSteps +} + +func part2(input string) int { + steps, graph := parseInput(input) + + locations := []string{} + for loc := range graph { + if loc[2:3] == "A" { + locations = append(locations, loc) + } + } + + /** + brute force doesn't work... need to figure out cycle times of each starting location + but they won't cycle just based on number of steps because of the weird L-R randomness + + so we can only rely on the "full cycle" of all steps before it loops + + - there are six starting locations + + NOTE: BIG assumptions based on KIND inputs + - assume that the Z-end locations will sync EXACTLY at the end of a cycle of steps + - after further analyzing logs of the end of each cycle, the entry point VERY kindly deposits us + at the very start of a cycle that will eventually end in a Z-end location + AAA -> MLM -> ... -> XKZ -> MLM -> ... -> XKZ -> MLM -> ... -> XKZ -> MLM + and this holds true for all six locations in my input + Therefore the cycles are not offset by a particular number of steps at the start to get to the cycle + such as START --> LOC1 --> LOC2 --> Start -> A + ^ | + | v + D <-- C + this makes the maths fairly straight forward with just having to find the LCM (least common multiple) + of all the cycle periods because that is when they will all sync up and land on a Z + */ + + numOfSteps := 0 + + locationCyclePeriods := []int{} + for cycle := 0; len(locations) > 0; cycle++ { + for _, dir := range steps { + for i, loc := range locations { + if dir == "L" { + locations[i] = graph[loc][0] + } else { + locations[i] = graph[loc][1] + } + } + numOfSteps++ + } + + // if any location is at a z-end at the end of a cycle, record the cycle time + // to do the final maths at the end + newLocations := []string{} + for _, loc := range locations { + if loc[2:3] == "Z" { + locationCyclePeriods = append(locationCyclePeriods, numOfSteps) + } else { + newLocations = append(newLocations, loc) + } + } + locations = newLocations + } + + // combine all into an LCM (helper function added to mathy package) + lcm := locationCyclePeriods[0] + for i := 1; i < len(locationCyclePeriods); i++ { + lcm = mathy.LeastCommonMultiple(lcm, locationCyclePeriods[i]) + } + + return lcm +} + +func parseInput(input string) (steps []string, graph map[string][]string) { + graph = map[string][]string{} + + parts := strings.Split(input, "\n\n") + steps = strings.Split(parts[0], "") + + for _, line := range strings.Split(parts[1], "\n") { + graph[line[0:3]] = []string{ + line[7:10], + line[12:15], + } + } + + return steps, graph +} diff --git a/2023/day08/main_test.go b/2023/day08/main_test.go new file mode 100644 index 0000000..5c0f7c1 --- /dev/null +++ b/2023/day08/main_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "testing" +) + +var example = `LLR + +AAA = (BBB, BBB) +BBB = (AAA, ZZZ) +ZZZ = (ZZZ, ZZZ)` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 6, + }, + { + name: "actual", + input: input, + want: 18673, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +var examplePart2 = `LR + +11A = (11B, XXX) +11B = (XXX, 11Z) +11Z = (11B, XXX) +22A = (22B, XXX) +22B = (22C, 22C) +22C = (22Z, 22Z) +22Z = (22B, 22B) +XXX = (XXX, XXX)` + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: examplePart2, + want: 6, + }, + { + name: "actual", + input: input, + want: 17972669116327, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day09/main.go b/2023/day09/main.go new file mode 100644 index 0000000..59d32be --- /dev/null +++ b/2023/day09/main.go @@ -0,0 +1,128 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + parsed := parseInput(input) + ans := 0 + for _, hist := range parsed { + ans += findNextValue(hist) + } + + return ans +} + +func findNextValue(history []int) int { + matrix := [][]int{ + history, + } + nonZeroFound := true + for nonZeroFound { + nonZeroFound = false + next := []int{} + + matrixHistoryRow := matrix[len(matrix)-1] + for i := 1; i < len(matrixHistoryRow); i++ { + prev := matrixHistoryRow[i-1] + curr := matrixHistoryRow[i] + next = append(next, curr-prev) + if next[len(next)-1] != 0 { + nonZeroFound = true + } + } + matrix = append(matrix, next) + } + + ans := 0 + for _, row := range matrix { + ans += row[len(row)-1] + } + + return ans +} + +func part2(input string) int { + parsed := parseInput(input) + ans := 0 + for _, hist := range parsed { + ans += findPrevValue(hist) + } + + return ans +} + +func findPrevValue(history []int) int { + matrix := [][]int{ + history, + } + nonZeroFound := true + for nonZeroFound { + nonZeroFound = false + next := []int{} + + matrixHistoryRow := matrix[len(matrix)-1] + for i := 1; i < len(matrixHistoryRow); i++ { + prev := matrixHistoryRow[i-1] + curr := matrixHistoryRow[i] + next = append(next, curr-prev) + if next[len(next)-1] != 0 { + nonZeroFound = true + } + } + matrix = append(matrix, next) + } + + ans := 0 + for r := len(matrix) - 1; r >= 0; r-- { + ans = matrix[r][0] - ans + } + + return ans +} + +func parseInput(input string) (ans [][]int) { + for _, line := range strings.Split(input, "\n") { + nums := []int{} + for _, str := range strings.Split(line, " ") { + nums = append(nums, cast.ToInt(str)) + } + ans = append(ans, nums) + } + return ans +} diff --git a/2023/day09/main_test.go b/2023/day09/main_test.go new file mode 100644 index 0000000..d3914c9 --- /dev/null +++ b/2023/day09/main_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "testing" +) + +var example = `0 3 6 9 12 15 +1 3 6 10 15 21 +10 13 16 21 30 45` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 114, + }, + { + name: "actual", + input: input, + want: 1782868781, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 2, + }, + { + name: "actual", + input: input, + want: 1057, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day10/main.go b/2023/day10/main.go new file mode 100644 index 0000000..5dafc85 --- /dev/null +++ b/2023/day10/main.go @@ -0,0 +1,260 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + ans := pipeMaze(input, part) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) +} + +var pipes = map[string][2][2]int{ + "|": { + {-1, 0}, // up + {1, 0}, // down + }, + "-": { + {0, -1}, // left + {0, 1}, //right + }, + "L": { + {-1, 0}, // up + {0, 1}, // right + }, + "J": { + {-1, 0}, // up + {0, -1}, // left + }, + "7": { + {0, -1}, // left + {1, 0}, // down + }, + "F": { + {0, 1}, // right + {1, 0}, // down + }, +} + +func pipeMaze(input string, part int) int { + grid := parseInput(input) + + var r, c int + for i, row := range grid { + for j, val := range row { + if val == "S" { + r = i + c = j + } + } + } + + fillGridLocation(grid, r, c) + + // traverse entire loop to determine length + loopCoords := map[[2]int]bool{} + toVisit := [][2]int{ + {r, c}, + } + for len(toVisit) > 0 { + coords := toVisit[0] + if loopCoords[coords] { + break + } + loopCoords[coords] = true + toVisit = toVisit[1:] + + // assumes loop is well formed, will cause a panic if not + diffs := pipes[grid[coords[0]][coords[1]]] + for _, diff := range diffs { + nextRow, nextCol := coords[0]+diff[0], coords[1]+diff[1] + if isInRange(grid, nextRow, nextCol) && !loopCoords[[2]int{nextRow, nextCol}] { + toVisit = append(toVisit, [2]int{nextRow, nextCol}) + } + } + + } + + if part == 1 { + return len(loopCoords) / 2 + } + + // part 2 + + // create copy of grid with all non-loop spots replaced with a period + reducedGrid := make([][]string, len(grid)) + for i := 0; i < len(grid); i++ { + for j := 0; j < len(grid[0]); j++ { + if loopCoords[[2]int{i, j}] { + reducedGrid[i] = append(reducedGrid[i], grid[i][j]) + } else { + reducedGrid[i] = append(reducedGrid[i], ".") + } + } + } + + // expand grid to double plus 2 in both dimensions to account for squeezing between pipes + // the plus two is to add an empty row/column on each side for easier traversing from the outside + expandedGrid := [][]string{} + expandedGrid = append(expandedGrid, make([]string, len(reducedGrid[0])*2+2)) + + for r, rows := range reducedGrid { + expandedGrid = append(expandedGrid, make([]string, len(reducedGrid[0])*2+2)) + for c, val := range rows { + expandedGrid[r*2+1][c*2+1] = val + } + // empty row + expandedGrid = append(expandedGrid, make([]string, len(reducedGrid[0])*2+2)) + } + + // fill gaps between loop coords so we have an encased area again + // we can naively try to fill in every empty spot because only ones with two valid connecting + // pipes will be filled + for r, rows := range expandedGrid { + for c, val := range rows { + if val == "" { + fillGridLocation(expandedGrid, r, c) + } + } + } + + // replacing empty strings with spaces makes the printout human readable + for r, rows := range expandedGrid { + for c, val := range rows { + if val == "" { + expandedGrid[r][c] = " " + } + } + } + + toVisit = [][2]int{ + {0, 0}, + } + seen := map[[2]int]bool{} + for len(toVisit) > 0 { + coords := toVisit[0] + toVisit = toVisit[1:] + if seen[coords] { + continue + } + seen[coords] = true + + // delete reachable dots + if expandedGrid[coords[0]][coords[1]] == "." { + expandedGrid[coords[0]][coords[1]] = " " + } + + for _, diff := range [][2]int{ + {-1, 0}, + {1, 0}, + {0, -1}, + {0, 1}, + } { + nextRow := coords[0] + diff[0] + nextCol := coords[1] + diff[1] + if isInRange(expandedGrid, nextRow, nextCol) { + if expandedGrid[nextRow][nextCol] == "." || expandedGrid[nextRow][nextCol] == " " { + toVisit = append(toVisit, [2]int{nextRow, nextCol}) + } + } + } + } + + // count remaining dots + var ans int + for _, rows := range expandedGrid { + for _, val := range rows { + if val == "." { + ans++ + } + } + } + + // for _, rows := range expandedGrid { + // fmt.Println(rows) + // } + + return ans +} + +func fillGridLocation(grid [][]string, r, c int) { + // inputs are nice and exactly two adjacent cells that connect to S + // check four directions from start + leftCol := c - 1 + rightCol := c + 1 + upRow := r - 1 + downRow := r + 1 + + var combinedString string + // check left for inRange and possible valid pipe types + if isInRange(grid, r, leftCol) && + (grid[r][leftCol] == "-" || grid[r][leftCol] == "L" || grid[r][leftCol] == "F") { + combinedString += "left" + } + // right + if isInRange(grid, r, rightCol) && + (grid[r][rightCol] == "-" || grid[r][rightCol] == "J" || grid[r][rightCol] == "7") { + combinedString += "right" + } + // up + if isInRange(grid, upRow, c) && + (grid[upRow][c] == "|" || grid[upRow][c] == "7" || grid[upRow][c] == "F") { + combinedString += "up" + } + if isInRange(grid, downRow, c) && + (grid[downRow][c] == "|" || grid[downRow][c] == "J" || grid[downRow][c] == "L") { + combinedString += "down" + } + + switch combinedString { + case "leftup": + grid[r][c] = "J" + case "leftdown": + grid[r][c] = "7" + case "rightup": + grid[r][c] = "L" + case "rightdown": + grid[r][c] = "F" + case "leftright": + grid[r][c] = "-" + case "updown": + grid[r][c] = "|" + // default: + // do not panic so we can use this function more naively for the expanded grid + // could return an error instead and choose to check it for part1 where we NEED it to find a result + // panic("ineligible configuration: " + combinedString) + } +} + +func isInRange(grid [][]string, row, col int) bool { + return row >= 0 && row < len(grid) && col >= 0 && col < len(grid[0]) +} + +func parseInput(input string) (ans [][]string) { + for _, line := range strings.Split(input, "\n") { + ans = append(ans, strings.Split(line, "")) + } + return ans +} diff --git a/2023/day10/main_test.go b/2023/day10/main_test.go new file mode 100644 index 0000000..c90d4ac --- /dev/null +++ b/2023/day10/main_test.go @@ -0,0 +1,123 @@ +package main + +import ( + "testing" +) + +var example = `..... +.S-7. +.|.|. +.L-J. +.....` + +var complexExample = `..F7. +.FJ|. +SJ.L7 +|F--J +LJ...` + +var examplePart2 = `........... +.S-------7. +.|F-----7|. +.||.....||. +.||.....||. +.|L-7.F-J|. +.|..|.|..|. +.L--J.L--J. +...........` +var examplePart2_2 = `.......... +.S------7. +.|F----7|. +.||OOOO||. +.||OOOO||. +.|L-7F-J|. +.|II||II|. +.L--JL--J. +..........` +var examplePart2_large = `.F----7F7F7F7F-7.... +.|F--7||||||||FJ.... +.||.FJ||||||||L7.... +FJL7L7LJLJ||LJ.L-7.. +L--J.L7...LJS7F-7L7. +....F-J..F7FJ|L7L7L7 +....L7.F7||L7|.L7L7| +.....|FJLJ|FJ|F7|.LJ +....FJL-7.||.||||... +....L---J.LJ.LJLJ...` +var examplePart2_larger = `FF7FSF7F7F7F7F7F---7 +L|LJ||||||||||||F--J +FL-7LJLJ||||||LJL-77 +F--JF--7||LJLJ7F7FJ- +L---JF-JLJ.||-FJLJJ7 +|F|F-JF---7F7-L7L|7| +|FFJF7L7F-JF7|JL---7 +7-L-JL7||F7|L7F-7F7| +L.L7LFJ|||||FJL7||LJ +L7JLJL-JLJLJL--JLJ.L` + +func Test_pipeMaze(t *testing.T) { + tests := []struct { + name string + input string + part int + want int + }{ + { + name: "example", + input: example, + part: 1, + want: 4, + }, + { + name: "complexExample", + input: complexExample, + part: 1, + want: 8, + }, + { + name: "actual part 1", + input: input, + part: 1, + want: 6773, + }, + + // part 2 + { + name: "examplePart2", + input: examplePart2, + part: 2, + want: 4, + }, + { + name: "examplePart2_2", + input: examplePart2_2, + part: 2, + want: 4, + }, + { + name: "examplePart2_large", + input: examplePart2_large, + part: 2, + want: 8, + }, + { + name: "examplePart2_larger", + input: examplePart2_larger, + part: 2, + want: 10, + }, + { + name: "actual part 2", + input: input, + part: 2, + want: 493, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pipeMaze(tt.input, tt.part); got != tt.want { + t.Errorf("pipeMaze() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day11/main.go b/2023/day11/main.go new file mode 100644 index 0000000..ebbae93 --- /dev/null +++ b/2023/day11/main.go @@ -0,0 +1,116 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input, 2) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part1(input, 1000000) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string, expansionFactor int) int { + grid := parseInput(input) + + // record which rows and cols are empty first + emptyRows := map[int]bool{} + emptyCols := map[int]bool{} + + for r := 0; r < len(grid); r++ { + galaxyFound := false + for c := 0; c < len(grid[0]); c++ { + if grid[r][c] != "." { + galaxyFound = true + break + } + } + if !galaxyFound { + emptyRows[r] = true + } + } + for c := 0; c < len(grid[0]); c++ { + galaxyFound := false + for r := 0; r < len(grid); r++ { + if grid[r][c] != "." { + galaxyFound = true + break + } + } + if !galaxyFound { + emptyCols[c] = true + } + } + + // traverse grid and calculate coordinates of each galaxy while accumulating the expanded rows/cols + // that can be added into the galaxy's coordinates as they're found + galaxyCoords := map[int][2]int{} + expandedRowsToAdd := 0 + for r := 0; r < len(grid); r++ { + if emptyRows[r] { + expandedRowsToAdd += expansionFactor - 1 + continue + } + + expendedColsToAdd := 0 + for c := 0; c < len(grid[0]); c++ { + if emptyCols[c] { + expendedColsToAdd += expansionFactor - 1 + continue + } + + if grid[r][c] == "#" { + galaxyCoords[len(galaxyCoords)] = [2]int{ + r + expandedRowsToAdd, + c + expendedColsToAdd, + } + } + } + } + + // shortest distance is basically manhattan distance, helper function handles absolute values + totalDistance := 0 + for i := 0; i < len(galaxyCoords); i++ { + for j := i + 1; j < len(galaxyCoords); j++ { + g1, g2 := galaxyCoords[i], galaxyCoords[j] + totalDistance += mathy.ManhattanDistance(g1[0], g1[1], g2[0], g2[1]) + } + } + + return totalDistance +} + +func parseInput(input string) (ans [][]string) { + for _, line := range strings.Split(input, "\n") { + ans = append(ans, strings.Split(line, "")) + } + return ans +} diff --git a/2023/day11/main_test.go b/2023/day11/main_test.go new file mode 100644 index 0000000..4f5e6c0 --- /dev/null +++ b/2023/day11/main_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "testing" +) + +var example = `...#...... +.......#.. +#......... +.......... +......#... +.#........ +.........# +.......... +.......#.. +#...#.....` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + expansionFactor int + want int + }{ + { + name: "example", + input: example, + expansionFactor: 2, + want: 374, + }, + { + name: "actual", + input: input, + expansionFactor: 2, + want: 9734203, + }, + + // part 2 + { + name: "example", + input: example, + expansionFactor: 10, + want: 1030, + }, + { + name: "example", + input: example, + expansionFactor: 100, + want: 8410, + }, + { + name: "actual", + input: input, + expansionFactor: 1000000, + want: 568914596391, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input, tt.expansionFactor); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day12/main.go b/2023/day12/main.go new file mode 100644 index 0000000..74b36b9 --- /dev/null +++ b/2023/day12/main.go @@ -0,0 +1,261 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + stringConditions := parseInput(input) + + ans := 0 + // brute force creating all possible combination per line + // then check each possibility + // input is 1000 lines with 10k ?'s total, so approx 10/line + // 2^10 = 1024 options per line approx. * 1000 = 1_024_000 checks total... seems ok... for part 1... + for _, sc := range stringConditions { + possibilities := generatePossibilities(sc.record) + + for _, p := range possibilities { + if checkIfSpringRecordFitsDamagedGroupCounts(p, sc.damagedGroupCounts) { + ans++ + } + } + } + + return ans +} + +func generatePossibilities(record []string) [][]string { + var recurse func(record []string, index int) [][]string + recurse = func(record []string, index int) [][]string { + if index == len(record) { + cp := make([]string, len(record)) + copy(cp, record) + return [][]string{cp} + } + + if record[index] != "?" { + return recurse(record, index+1) + } + possibilities := [][]string{} + record[index] = "#" + possibilities = append(possibilities, recurse(record, index+1)...) + + record[index] = "." + possibilities = append(possibilities, recurse(record, index+1)...) + + record[index] = "?" + + return possibilities + } + + return recurse(record, 0) +} + +func checkIfSpringRecordFitsDamagedGroupCounts(condition []string, damagedGroupCounts []int) bool { + consecutiveDamagedCount := 0 + foundDamageGroupCounts := []int{} + for _, cond := range condition { + if cond == "." { + if consecutiveDamagedCount != 0 { + foundDamageGroupCounts = append(foundDamageGroupCounts, consecutiveDamagedCount) + } + consecutiveDamagedCount = 0 + } else { + consecutiveDamagedCount++ + } + } + if consecutiveDamagedCount != 0 { + foundDamageGroupCounts = append(foundDamageGroupCounts, consecutiveDamagedCount) + } + + if len(damagedGroupCounts) == len(foundDamageGroupCounts) { + for i := 0; i < len(damagedGroupCounts); i++ { + if damagedGroupCounts[i] != foundDamageGroupCounts[i] { + return false + } + } + return true + } + + return false +} + +func part2(input string) int { + // brute force will not work for part 2 presumably. 2^10 becomes 2^50 which is 1 trillion times larger? + + stringConditions := parseInput(input) + + // hacky hacky way to update string conditions... + for i, sc := range stringConditions { + for x := 0; x < 4; x++ { + stringConditions[i].record = append(stringConditions[i].record, "?") + stringConditions[i].record = append(stringConditions[i].record, sc.record...) + stringConditions[i].damagedGroupCounts = append(stringConditions[i].damagedGroupCounts, sc.damagedGroupCounts...) + } + // adding a "." at the end helps future logic ensure that the final damaged group will be ended + stringConditions[i].record = append(stringConditions[i].record, ".") + } + + ans := 0 + for _, sc := range stringConditions { + memoOfPossibilities := map[[3]int]int{} + ans += memo(sc, 0, 0, 0, memoOfPossibilities) + } + + return ans +} + +func memo(sc springCondition, index, doneGroups, currentGroupSize int, memoOfPossibilities map[[3]int]int) int { + // key of 0, 0, 0 holds final answer of possible results + key := [3]int{index, doneGroups, currentGroupSize} + + if ans, ok := memoOfPossibilities[key]; ok { + return ans + } + + // if the end of the record is reached, and all damaged groups are accounted for + // do not need to check for currentGroupSize because of the trailing "." that was added + if index == len(sc.record) && doneGroups == len(sc.damagedGroupCounts) { + memoOfPossibilities[key] = 1 + return 1 + } + + // any other scenario where we've reached the final index means this possibility is invalid + if index == len(sc.record) { + memoOfPossibilities[key] = 0 + return 0 + } + + // damaged spring groups are all accounted for but ran into an additional broken spring, + // this branch is not valid + if doneGroups == len(sc.damagedGroupCounts) && sc.record[index] == "#" { + memoOfPossibilities[key] = 0 + return 0 + } + + // handle ".", "#" or "?" + possibilities := 0 + if sc.record[index] == "." { + // end the previous group + if index == 0 { + possibilities = memo(sc, index+1, 0, 0, memoOfPossibilities) + } else if currentGroupSize == 0 { + possibilities = memo(sc, index+1, doneGroups, 0, memoOfPossibilities) + } else if currentGroupSize != 0 { + // we have a non-zero current group size so if all damaged groups are accounted for, + // there are no possibilities left for this branch + if doneGroups == len(sc.damagedGroupCounts) { + possibilities = 0 + } else { + // not all damaged groups are accounted for + // if the current group is the right size, recurse; if not, then zero possibilities remain + if currentGroupSize == sc.damagedGroupCounts[doneGroups] { + possibilities = memo(sc, index+1, doneGroups+1, 0, memoOfPossibilities) + } else if currentGroupSize != sc.damagedGroupCounts[doneGroups] { + // last group is the wrong size, zero possibilities for this branch + possibilities = 0 + } + } + } + + } else if sc.record[index] == "#" { + // build group + currentGroupSize++ + // if current group size is too big, this branch has zero possibilities + if currentGroupSize > sc.damagedGroupCounts[doneGroups] { + possibilities = 0 + } else { + possibilities = memo(sc, index+1, doneGroups, currentGroupSize, memoOfPossibilities) + } + + } else if sc.record[index] == "?" { + // ? + // add two possibilities: a damaged spring or OK spring + + // if it is a # + // do not need to account for if the group is too big here, it'll be handled by a future "#" + // check or a ".", again part of the reason why a trailing period was added + possibilities += memo(sc, index+1, doneGroups, currentGroupSize+1, memoOfPossibilities) + // currentGroupSize-- + + // take as . + // same code as above for if "." block, but possibilities is added to instead of just set + if index == 0 { + possibilities += memo(sc, index+1, 0, 0, memoOfPossibilities) + } else if currentGroupSize == 0 { + possibilities += memo(sc, index+1, doneGroups, currentGroupSize, memoOfPossibilities) + } else { + if doneGroups == len(sc.damagedGroupCounts) { + possibilities += 0 + } else { + if currentGroupSize == sc.damagedGroupCounts[doneGroups] { + possibilities += memo(sc, index+1, doneGroups+1, 0, memoOfPossibilities) + } else if currentGroupSize != sc.damagedGroupCounts[doneGroups] { + possibilities += 0 + } + } + } + + } else { + panic("unexpected string condition record character: " + sc.record[index]) + } + + memoOfPossibilities[key] = possibilities + return possibilities +} + +type springCondition struct { + record []string + damagedGroupCounts []int +} + +func parseInput(input string) (ans []springCondition) { + for _, line := range strings.Split(input, "\n") { + parts := strings.Split(line, " ") + sc := springCondition{ + record: strings.Split(parts[0], ""), + damagedGroupCounts: []int{}, + } + for _, str := range strings.Split(parts[1], ",") { + sc.damagedGroupCounts = append(sc.damagedGroupCounts, cast.ToInt(str)) + } + ans = append(ans, sc) + } + + return ans +} diff --git a/2023/day12/main_test.go b/2023/day12/main_test.go new file mode 100644 index 0000000..09bf078 --- /dev/null +++ b/2023/day12/main_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "testing" +) + +var example = `???.### 1,1,3 +.??..??...?##. 1,1,3 +?#?#?#?#?#?#?#? 1,3,1,6 +????.#...#... 4,1,1 +????.######..#####. 1,6,5 +?###???????? 3,2,1` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 21, + }, + { + name: "actual", + input: input, + want: 7792, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 525152, + }, + { + name: "actual", + input: input, + want: 13012052341533, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day13/main.go b/2023/day13/main.go new file mode 100644 index 0000000..ec94bc4 --- /dev/null +++ b/2023/day13/main.go @@ -0,0 +1,165 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + patterns := parseInput(input) + + ans := 0 + for _, pattern := range patterns { + maybeMirrorRow := findMirrorRow(pattern, -1) + if maybeMirrorRow != -1 { + ans += 100 * maybeMirrorRow + } else { + maybeMirrorCol := findMirrorCol(pattern, -1) + if maybeMirrorCol == -1 { + panic("did not find mirror row or col") + } + ans += maybeMirrorCol + } + } + + return ans +} + +// returns the zero-index of the line, if the line is between index 3 and 4, it returns 4 +// ignoreRow is for part2 where we want to ignore the original mirrored row because it may still be +// valid in the un-smudged pattern +func findMirrorRow(pattern [][]string, ignoreRow int) int { + // combine the string slices into a string so they're easier to compare + combinedRows := []string{} + for _, row := range pattern { + combinedRows = append(combinedRows, strings.Join(row, "")) + } + + for i := 1; i < len(combinedRows); i++ { + mismatchFound := false + for offset := 1; i-offset >= 0 && i+offset-1 < len(combinedRows); offset++ { + // fmt.Println("combined row indexes", i-offset, i+offset-1) + // fmt.Println(combinedRows[i-offset], "\n", combinedRows[i+offset-1]) + if combinedRows[i-offset] != combinedRows[i+offset-1] { + mismatchFound = true + // fmt.Println("mismatch found") + break + } + } + + if !mismatchFound { + if i != ignoreRow { + return i + } + } + } + // none found + return -1 +} + +func findMirrorCol(pattern [][]string, ignoreCol int) int { + // rotate the grid, maintaining the indices for easier maths later + // then just pass it into the findMirrorRow func + rotatedGrid := [][]string{} + for c := 0; c < len(pattern[0]); c++ { + newRow := []string{} + for r := 0; r < len(pattern); r++ { + newRow = append(newRow, pattern[r][c]) + } + rotatedGrid = append(rotatedGrid, newRow) + } + + return findMirrorRow(rotatedGrid, ignoreCol) +} + +func part2(input string) int { + patterns := parseInput(input) + + ans := 0 + + for _, pattern := range patterns { + + // store the original row and col so they can be ignored in the find mirror row func + originalMirrorRow := findMirrorRow(pattern, -1) + originalMirrorCol := findMirrorCol(pattern, -1) + + traverse: + // labels suck but without the breaks this all has to go into a separate function which is + // arguably less readable. and the break is necessary to not double count reflections + for r, row := range pattern { + for c, val := range row { + if val == "." { + pattern[r][c] = "#" + if maybeMirrorRow := findMirrorRow(pattern, originalMirrorRow); maybeMirrorRow != -1 { + ans += 100 * maybeMirrorRow + break traverse + } + if maybeMirrorCol := findMirrorCol(pattern, originalMirrorCol); maybeMirrorCol != -1 { + ans += maybeMirrorCol + break traverse + } + pattern[r][c] = "." + } else if val == "#" { + pattern[r][c] = "." + if maybeMirrorRow := findMirrorRow(pattern, originalMirrorRow); maybeMirrorRow != -1 { + ans += 100 * maybeMirrorRow + break traverse + } + if maybeMirrorCol := findMirrorCol(pattern, originalMirrorCol); maybeMirrorCol != -1 { + ans += maybeMirrorCol + break traverse + } + pattern[r][c] = "#" + + } else { + panic("expected input: " + val) + } + } + } + } + + return ans +} + +func parseInput(input string) (ans [][][]string) { + for _, section := range strings.Split(input, "\n\n") { + grid := [][]string{} + for _, line := range strings.Split(section, "\n") { + grid = append(grid, strings.Split(line, "")) + } + ans = append(ans, grid) + } + return ans +} diff --git a/2023/day13/main_test.go b/2023/day13/main_test.go new file mode 100644 index 0000000..83c6e0f --- /dev/null +++ b/2023/day13/main_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "testing" +) + +var example = `#.##..##. +..#.##.#. +##......# +##......# +..#.##.#. +..##..##. +#.#.##.#. + +#...##..# +#....#..# +..##..### +#####.##. +#####.##. +..##..### +#....#..#` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 405, + }, + { + name: "actual", + input: input, + want: 30575, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 400, + }, + { + name: "actual", + input: input, + want: 37478, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day14/main.go b/2023/day14/main.go new file mode 100644 index 0000000..f07e2a7 --- /dev/null +++ b/2023/day14/main.go @@ -0,0 +1,185 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + grid := parseInput(input) + + // tilt north, all O's roll to the top or to the next # + tiltNorth(grid) + + // then calculate total load (total rows) - (row index) per rock + ans := 0 + for r, row := range grid { + for _, val := range row { + if val == "O" { + ans += len(grid) - r + } + } + } + + return ans +} + +func part2(input string) int { + grid := parseInput(input) + + seenStates := map[string]int{} + + cycles := 1000000000 + for c := 0; c < cycles; c++ { + key := stringifyStringGrid(grid) + if lastIndex, ok := seenStates[key]; ok { + cyclePeriod := c - lastIndex + for c+cyclePeriod < cycles { + c += cyclePeriod + } + } + seenStates[key] = c + + // 1 cycle = tilt N, W, S, E + tiltNorth(grid) + tiltWest(grid) + tiltSouth(grid) + tiltEast(grid) + } + + ans := 0 + for r, row := range grid { + for _, val := range row { + if val == "O" { + ans += len(grid) - r + } + } + } + + // 99841 too low + return ans +} + +func tiltNorth(grid [][]string) { + for r, row := range grid { + for c, val := range row { + if val == "O" { + for nextRow := r - 1; nextRow >= 0; nextRow-- { + // can only fall north if nextRow is an empty space + if grid[nextRow][c] == "." { + grid[nextRow][c] = "O" + grid[nextRow+1][c] = "." + } else { + break + } + } + } + } + } +} + +func tiltSouth(grid [][]string) { + for r := len(grid) - 1; r >= 0; r-- { + for c := range len(grid[0]) { + val := grid[r][c] + if val == "O" { + for nextRow := r + 1; nextRow < len(grid); nextRow++ { + // can only fall north if nextRow is an empty space + if grid[nextRow][c] == "." { + grid[nextRow][c] = "O" + grid[nextRow-1][c] = "." + } else { + break + } + } + } + } + } +} + +func tiltEast(grid [][]string) { + for c := len(grid[0]) - 1; c >= 0; c-- { + for r := range grid { + val := grid[r][c] + + if val == "O" { + for nextCol := c + 1; nextCol < len(grid[0]); nextCol++ { + // can only fall north if nextCol is an empty space + if grid[r][nextCol] == "." { + grid[r][nextCol] = "O" + grid[r][nextCol-1] = "." + } else { + break + } + } + } + } + } +} + +func tiltWest(grid [][]string) { + for c := range len(grid[0]) { + for r := range grid { + val := grid[r][c] + + if val == "O" { + for nextCol := c - 1; nextCol >= 0; nextCol-- { + // can only fall north if nextCol is an empty space + if grid[r][nextCol] == "." { + grid[r][nextCol] = "O" + grid[r][nextCol+1] = "." + } else { + break + } + } + } + } + } +} + +func stringifyStringGrid(grid [][]string) string { + ans := "" + for _, row := range grid { + ans += strings.Join(row, "") + } + return ans +} + +func parseInput(input string) (ans [][]string) { + for _, line := range strings.Split(input, "\n") { + ans = append(ans, strings.Split(line, "")) + } + return ans +} diff --git a/2023/day14/main_test.go b/2023/day14/main_test.go new file mode 100644 index 0000000..5c4bb73 --- /dev/null +++ b/2023/day14/main_test.go @@ -0,0 +1,68 @@ +package main + +import ( + "testing" +) + +var example = `O....#.... +O.OO#....# +.....##... +OO.#O....O +.O.....O#. +O.#..O.#.# +..O..#O..O +.......O.. +#....###.. +#OO..#....` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 136, + }, + { + name: "actual", + input: input, + want: 108840, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 64, + }, + { + name: "actual", + input: input, + want: 103445, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day15/main.go b/2023/day15/main.go new file mode 100644 index 0000000..a507969 --- /dev/null +++ b/2023/day15/main.go @@ -0,0 +1,132 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + parsed := parseInput(input) + ans := 0 + for _, step := range parsed { + ans += hash(step) + } + + return ans +} + +func hash(step string) int { + ans := 0 + for _, char := range strings.Split(step, "") { + asciiVal := cast.ToASCIICode(char) + ans += asciiVal + ans *= 17 + ans %= 256 + } + return ans +} + +func part2(input string) int { + boxes := make([]box, 256) + // optimization to keep a linked list within in box, but likely not necessary... + labelToBoxIndex := map[string]int{} + + steps := parseInput(input) + + for _, step := range steps { + if strings.Contains(step, "=") { + parts := strings.Split(step, "=") + label := parts[0] + focalLength := cast.ToInt(parts[1]) + + boxIndex := hash(label) + + if oldBoxIndex, ok := labelToBoxIndex[label]; ok { + if oldBoxIndex != boxIndex { + panic("hashes should be the same...") + } + // iterate and update focalLength of found box + for i := range len(boxes[boxIndex]) { + if boxes[boxIndex][i].label == label { + boxes[boxIndex][i].focalLength = focalLength + } + } + } else { + boxes[boxIndex] = append(boxes[boxIndex], lense{ + label: label, + focalLength: focalLength, + }) + labelToBoxIndex[label] = boxIndex + } + + } else if strings.Contains(step, "-") { + label := step[:len(step)-1] + if boxIndex, ok := labelToBoxIndex[label]; ok { + // switch it all the way to the end + for i := range len(boxes[boxIndex]) - 1 { + if boxes[boxIndex][i].label == label { + boxes[boxIndex][i], boxes[boxIndex][i+1] = boxes[boxIndex][i+1], boxes[boxIndex][i] + } + } + // cut off end, remove from map + boxes[boxIndex] = boxes[boxIndex][:len(boxes[boxIndex])-1] + delete(labelToBoxIndex, label) + } + } else { + panic("unexpected step format: " + step) + } + } + + ans := 0 + + for boxIndex, box := range boxes { + for lenseIndex, lense := range box { + ans += (boxIndex + 1) * (lenseIndex + 1) * lense.focalLength + } + } + + return ans +} + +type lense struct { + label string + focalLength int +} +type box []lense + +func parseInput(input string) (ans []string) { + return strings.Split(input, ",") +} diff --git a/2023/day15/main_test.go b/2023/day15/main_test.go new file mode 100644 index 0000000..7638bc4 --- /dev/null +++ b/2023/day15/main_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "testing" +) + +var example = `rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 1320, + }, + { + name: "actual", + input: input, + want: 507666, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 145, + }, + { + name: "actual", + input: input, + want: 233537, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day16/main.go b/2023/day16/main.go new file mode 100644 index 0000000..2fd0f85 --- /dev/null +++ b/2023/day16/main.go @@ -0,0 +1,236 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + grid := parseInput(input) + + return calcEnergizedTiles(grid, beamHead{ + velocity: right, + row: 0, + col: 0, + }) +} + +func part2(input string) int { + grid := parseInput(input) + best := 0 + for r := range len(grid) { + // starting from left of grid, headed right + startBeam := beamHead{ + velocity: right, + row: r, + col: 0, + } + // max is a go 1.21 addition: https://pkg.go.dev/builtin#max + best = max(best, calcEnergizedTiles(grid, startBeam)) + + // starting from right of grid, headed left + startBeam = beamHead{ + velocity: left, + row: r, + col: len(grid[0]) - 1, + } + best = max(best, calcEnergizedTiles(grid, startBeam)) + } + + for c := range len(grid[0]) { + // starting from top of grid, headed down + startBeam := beamHead{ + velocity: down, + row: 0, + col: c, + } + best = max(best, calcEnergizedTiles(grid, startBeam)) + + // starting from bottom of grid, headed up + startBeam = beamHead{ + velocity: up, + row: len(grid) - 1, + col: c, + } + best = max(best, calcEnergizedTiles(grid, startBeam)) + } + + return best +} + +func calcEnergizedTiles(grid [][]string, beam beamHead) int { + // need to track multiple beams because they can split and generate multiple + beams := []beamHead{beam} + + // [4]bool represents being hit from left, right, up and down respectively + // need to track direction so that we can terminate beams that are cyclical + hitGrid := [][][4]bool{} + for range grid { + hitGrid = append(hitGrid, make([][4]bool, len(grid[0]))) + } + + for len(beams) > 0 { + b := beams[0] + beams = beams[1:] + + skip := false + for vel, hitGridIndex := range velToHitGridIndex { + if b.velocity == vel && hitGrid[b.row][b.col][hitGridIndex] { + skip = true + } + } + if skip { + continue + } + + // record direction hit in hit grid + hitGrid[b.row][b.col][velToHitGridIndex[b.velocity]] = true + + cell := grid[b.row][b.col] + nextVelocities, ok := mirrorToNextVelocities[cell][b.velocity] + if !ok { + panic("no nextVelocities found for cell type: " + cell) + } + for _, nextVelocity := range nextVelocities { + nextRow := b.row + nextVelocity[0] + nextCol := b.col + nextVelocity[1] + + if nextRow < 0 || nextRow >= len(grid) || + nextCol < 0 || nextCol >= len(grid[0]) { + continue + } + + beams = append(beams, beamHead{ + velocity: nextVelocity, + row: nextRow, + col: nextCol, + }) + } + } + + energizedTiles := 0 + for r := range len(hitGrid) { + for c := range len(hitGrid[0]) { + for _, dir := range hitGrid[r][c] { + if dir { + energizedTiles++ + break + } + } + } + } + return energizedTiles +} + +var left = [2]int{0, -1} +var right = [2]int{0, 1} +var up = [2]int{-1, 0} +var down = [2]int{1, 0} + +var velToHitGridIndex = map[[2]int]int{ + left: 0, + right: 1, + up: 2, + down: 3, +} + +var mirrorToNextVelocities = map[string]map[[2]int][][2]int{ + ".": { + left: {left}, + right: {right}, + up: {up}, + down: {down}, + }, + "/": { + left: {down}, + down: {left}, + up: {right}, + right: {up}, + }, + "\\": { + left: {up}, + up: {left}, + down: {right}, + right: {down}, + }, + "|": { + left: {up, down}, + right: {up, down}, + up: {up}, + down: {down}, + }, + "-": { + left: {left}, + right: {right}, + up: {left, right}, + down: {left, right}, + }, +} + +type beamHead struct { + velocity [2]int + row, col int +} + +func (b beamHead) String() string { + return fmt.Sprintf("vel: %v, coords: %d, %d", b.velocity, b.row, b.col) +} + +// for debugging +func condenseHitGrid(grid [][][4]bool) [][]int { + ans := [][]int{} + for range grid { + ans = append(ans, make([]int, len(grid[0]))) + } + + for r, row := range grid { + for c, sli := range row { + for _, val := range sli { + if val { + ans[r][c]++ + } + } + } + } + + return ans +} + +func parseInput(input string) (ans [][]string) { + for _, line := range strings.Split(input, "\n") { + ans = append(ans, strings.Split(line, "")) + } + return ans +} diff --git a/2023/day16/main_test.go b/2023/day16/main_test.go new file mode 100644 index 0000000..d4fab22 --- /dev/null +++ b/2023/day16/main_test.go @@ -0,0 +1,68 @@ +package main + +import ( + "testing" +) + +var example = `.|...\.... +|.-.\..... +.....|-... +........|. +.......... +.........\ +..../.\\.. +.-.-/..|.. +.|....-|.\ +..//.|....` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 46, + }, + { + name: "actual", + input: input, + want: 7046, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 51, + }, + { + name: "actual", + input: input, + want: 7313, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day17/main.go b/2023/day17/main.go new file mode 100644 index 0000000..4a141c5 --- /dev/null +++ b/2023/day17/main.go @@ -0,0 +1,153 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/data-structures/heap" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := clumsyCart(input, 1, 3) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := clumsyCart(input, 4, 10) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func clumsyCart(input string, minMoves, maxMoves int) int { + grid := parseInput(input) + + minHeap := heap.NewMinHeap() + minHeap.Add(bfsNode{ + heatLoss: 0, + row: 0, + col: 0, + lastDir: right, + debugPath: "", + }) + minHeap.Add(bfsNode{ + heatLoss: 0, + row: 0, + col: 0, + lastDir: down, + debugPath: "", + }) + + // store lowest heat loss for each coordinate for each direction, slightly suboptimal + // because it can be divided into vertical and horizontal + // instead of L, R, U, D. But it's a constant time optimization but it runs fast enough + cache := map[string]int{} + + for minHeap.Length() > 0 { + node := minHeap.Remove().(bfsNode) + + key := fmt.Sprintf("%d %d - %v", node.row, node.col, node.lastDir) + if val, ok := cache[key]; ok { + if node.heatLoss >= val { + // exit if the current heatLoss isn't better + continue + } else { + cache[key] = node.heatLoss + } + } else { + cache[key] = node.heatLoss + } + + if node.row == len(grid)-1 && node.col == len(grid[0])-1 { + return node.heatLoss + } + + // just add a node for each vertical direction, then those will move vertically as well + // which covers all possibilities + for _, nextDir := range verticalTurns[node.lastDir] { + summedHeatLoss := 0 + for i := 1; i <= maxMoves; i++ { + nextRow := node.row + nextDir[0]*i + nextCol := node.col + nextDir[1]*i + + // skip if out of range + if nextRow < 0 || nextRow >= len(grid) || nextCol < 0 || nextCol >= len(grid[0]) { + continue + } + + summedHeatLoss += grid[nextRow][nextCol] + + // do not add to heap if the cart has moved less than the minimum required moves (part 2) + if i < minMoves { + continue + } + + minHeap.Add(bfsNode{ + heatLoss: node.heatLoss + summedHeatLoss, + row: nextRow, + col: nextCol, + lastDir: nextDir, + debugPath: node.debugPath + fmt.Sprintf("%d,%d ", nextRow, nextCol), + }) + } + } + } + + panic("should return from heap processing") +} + +type bfsNode struct { + heatLoss int + row, col int + lastDir direction + debugPath string +} + +func (b bfsNode) Value() int { + return b.heatLoss +} + +type direction [2]int + +var up = direction{-1, 0} +var down = direction{1, 0} +var left = direction{0, -1} +var right = direction{0, 1} + +var verticalTurns = map[direction][2]direction{ + up: {left, right}, + down: {left, right}, + left: {up, down}, + right: {up, down}, +} + +func parseInput(input string) (ans [][]int) { + for _, line := range strings.Split(input, "\n") { + row := []int{} + for _, str := range strings.Split(line, "") { + row = append(row, cast.ToInt(str)) + } + ans = append(ans, row) + } + return ans +} diff --git a/2023/day17/main_test.go b/2023/day17/main_test.go new file mode 100644 index 0000000..b418f99 --- /dev/null +++ b/2023/day17/main_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "testing" +) + +var example = `2413432311323 +3215453535623 +3255245654254 +3446585845452 +4546657867536 +1438598798454 +4457876987766 +3637877979653 +4654967986887 +4564679986453 +1224686865563 +2546548887735 +4322674655533` + +func Test_clumsyCart(t *testing.T) { + tests := []struct { + name string + input string + minMoves int + maxMoves int + want int + }{ + { + name: "example", + input: example, + minMoves: 1, + maxMoves: 3, + want: 102, + }, + { + name: "actual", + input: input, + minMoves: 1, + maxMoves: 3, + want: 1001, + }, + { + name: "example_part2", + input: example, + minMoves: 4, + maxMoves: 10, + want: 94, + }, + { + name: "actual", + input: input, + minMoves: 1, + maxMoves: 3, + want: 1197, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := clumsyCart(tt.input, tt.minMoves, tt.maxMoves); got != tt.want { + t.Errorf("clumsyCart() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day18/main.go b/2023/day18/main.go new file mode 100644 index 0000000..a850eba --- /dev/null +++ b/2023/day18/main.go @@ -0,0 +1,217 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strconv" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + digInstructions := parseInput(input) + + trenchCoords := getTrenchCoords(digInstructions) + + containedCoords := getContainedCoords(trenchCoords) + + return len(containedCoords) + len(trenchCoords) +} + +func getTrenchCoords(digInstructions []digInstruction) map[[2]int]bool { + trenchCoords := map[[2]int]bool{ + {0, 0}: true, + } + + var row, col int + diffs := map[string][2]int{ + "L": {0, -1}, + "R": {0, 1}, + "U": {-1, 0}, + "D": {1, 0}, + } + + for _, inst := range digInstructions { + for i := 1; i <= inst.length; i++ { + row += diffs[inst.dir][0] + col += diffs[inst.dir][1] + trenchCoords[[2]int{row, col}] = true + } + } + return trenchCoords +} + +func getContainedCoords(trenchCoords map[[2]int]bool) map[[2]int]bool { + // check around a coordinate that's part of a straight line for a cell that _could_ be contained + // straight lines will have one side that's in and one that is out + // we'll only check vertical lines to make it easier to code... + var testCoords [][2]int + + for coord := range trenchCoords { + upCoords := [2]int{coord[0] - 1, coord[1]} + downCoords := [2]int{coord[0] + 1, coord[1]} + leftCoords := [2]int{coord[0], coord[1] - 1} + rightCoords := [2]int{coord[0], coord[1] + 1} + + if trenchCoords[upCoords] && trenchCoords[downCoords] && + !trenchCoords[leftCoords] && !trenchCoords[rightCoords] { + // part of vertical line + testCoords = append(testCoords, leftCoords, rightCoords) + break + } + } + + // calculate the max size that can be contained (equal to the box containing all the coordinates) + var ( + left = testCoords[0][1] + right = testCoords[0][1] + top = testCoords[0][0] + bottom = testCoords[0][0] + ) + for coords := range trenchCoords { + left = min(left, coords[1]) + right = max(right, coords[1]) + top = min(top, coords[0]) + bottom = max(bottom, coords[0]) + } + + maxContainedSize := (right - left + 1) * (bottom - top + 1) + + for _, coord := range testCoords { + queue := [][2]int{coord} + seen := map[[2]int]bool{} + + for len(queue) > 0 && len(seen) < maxContainedSize { + current := queue[0] + queue = queue[1:] + + if seen[current] { + continue + } + seen[current] = true + + for _, diff := range [][2]int{ + {-1, 0}, + {1, 0}, + {0, -1}, + {0, 1}, + } { + nextRow := current[0] + diff[0] + nextCol := current[1] + diff[1] + nextCoord := [2]int{nextRow, nextCol} + // if already seen or it's part of the trench, skip + if trenchCoords[nextCoord] || seen[nextCoord] { + continue + } + // otherwise add it to be searched + queue = append(queue, nextCoord) + } + } + + if len(queue) == 0 { + return seen + } + } + panic("should return from loop") +} + +func part2(input string) int { + digInstructions := parseInput(input) + + vertices := [][2]int{} + currentPoint := [2]int{0, 0} + + for _, inst := range digInstructions { + hex := inst.color[1 : len(inst.color)-1] + dirCode := inst.color[len(inst.color)-1:] + + convInt, err := strconv.ParseInt(hex, 16, 0) + if err != nil { + panic(err.Error()) + } + + switch dirCode { + case "0": // R + currentPoint[1] += int(convInt) + case "1": // D + currentPoint[0] += int(convInt) + case "2": // L + currentPoint[1] -= int(convInt) + case "3": // U + currentPoint[0] -= int(convInt) + } + vertices = append(vertices, currentPoint) + } + + return shoelace(vertices) + 1 +} + +func shoelace(coordinates [][2]int) int { + area := 0 + + for i := 0; i < len(coordinates); i++ { + coordA := coordinates[i] + coordB := coordinates[(i+1)%(len(coordinates))] + + area += (coordA[1] * coordB[0]) - (coordB[1] * coordA[0]) + + max(abs(coordA[0]-coordB[0]), abs(coordA[1]-coordB[1])) + } + + return area / 2 +} + +func abs(i int) int { + if i < 0 { + return -i + } + return i +} + +type digInstruction struct { + dir string + length int + color string +} + +func parseInput(input string) (ans []digInstruction) { + for _, line := range strings.Split(input, "\n") { + parts := strings.Split(line, " ") + ans = append(ans, digInstruction{ + dir: parts[0], + length: cast.ToInt(parts[1]), + color: parts[2][1 : len(parts[2])-1], + }) + } + return ans +} diff --git a/2023/day18/main_test.go b/2023/day18/main_test.go new file mode 100644 index 0000000..2896995 --- /dev/null +++ b/2023/day18/main_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "testing" +) + +var example = `R 6 (#70c710) +D 5 (#0dc571) +L 2 (#5713f0) +D 2 (#d2c081) +R 2 (#59c680) +D 2 (#411b91) +L 5 (#8ceee2) +U 2 (#caa173) +L 1 (#1b58a2) +U 2 (#caa171) +R 2 (#7807d2) +U 3 (#a77fa3) +L 2 (#015232) +U 2 (#7a21e3)` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 62, + }, + { + name: "actual", + input: input, + want: 47527, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 952408144115, + }, + { + name: "actual", + input: input, + want: 52240187443190, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day19/main.go b/2023/day19/main.go new file mode 100644 index 0000000..cec099d --- /dev/null +++ b/2023/day19/main.go @@ -0,0 +1,240 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + parts, workflowsMap := parseInput(input) + + ans := 0 + + for _, p := range parts { + currentWorkflowName := "in" + for currentWorkflowName != "A" && currentWorkflowName != "R" { + wf := workflowsMap[currentWorkflowName] + for _, rule := range wf.ruleStrings { + if !strings.Contains(rule, ":") { + currentWorkflowName = rule + break + } + + colonSplit := strings.Split(rule, ":") + output := colonSplit[1] + + if strings.Contains(colonSplit[0], "<") { + conditionalParts := strings.Split(colonSplit[0], "<") + if p[conditionalParts[0]] < cast.ToInt(conditionalParts[1]) { + currentWorkflowName = output + break + } + } else if strings.Contains(colonSplit[0], ">") { + conditionalParts := strings.Split(colonSplit[0], ">") + if p[conditionalParts[0]] > cast.ToInt(conditionalParts[1]) { + currentWorkflowName = output + break + } + } else { + panic("unexpected workflow rule conditional: " + rule) + } + } + } + + if currentWorkflowName == "A" { + ans += p.sumRatings() + } + } + + return ans +} + +type part map[string]int + +func (p part) sumRatings() int { + total := 0 + for _, v := range p { + total += v + } + return total +} + +type workflow struct { + name string + ruleStrings []string +} + +func parseInput(input string) (parts []part, workflowsMap map[string]workflow) { + inputParts := strings.Split(input, "\n\n") + + workflowsMap = map[string]workflow{} + + // process workflows + for _, line := range strings.Split(inputParts[0], "\n") { + lineParts := strings.Split(line, "{") + wf := workflow{ + name: lineParts[0], + ruleStrings: strings.Split(lineParts[1][:len(lineParts[1])-1], ","), + } + workflowsMap[wf.name] = wf + } + + for _, line := range strings.Split(inputParts[1], "\n") { + withoutBraces := line[1 : len(line)-1] + p := part{} + for _, ratingStr := range strings.Split(withoutBraces, ",") { + ratingParts := strings.Split(ratingStr, "=") + p[ratingParts[0]] = cast.ToInt(ratingParts[1]) + } + parts = append(parts, p) + } + + return parts, workflowsMap +} + +func part2(input string) int { + _, workflowsMap := parseInput(input) + + // 1 to 4000 bounds for each rating... + boundedParts := map[string][2]int{ + "x": {1, 4000}, + "m": {1, 4000}, + "a": {1, 4000}, + "s": {1, 4000}, + } + + return updatePartBoundsAndSplit(boundedParts, workflowsMap, "in", 0) +} + +func updatePartBoundsAndSplit(boundedParts map[string][2]int, workflowsMap map[string]workflow, currentWorkflow string, debugDepth int) int { + if currentWorkflow == "R" { + return 0 + } + if currentWorkflow == "A" { + product := 1 + for _, bounds := range boundedParts { + product *= bounds[1] - bounds[0] + 1 + } + return product + } + + // split based on rules... + total := 0 + + // for each rule, + // the rule either passes and moves onto a different workflow, + // or fails and checks the next rule + // need to sum up both forks + // + // passing is handled via recursion, failing is handled via looping to the next rule + // in both cases the bounds need to be updated + for _, rule := range workflowsMap[currentWorkflow].ruleStrings { + // just the next workflow to go after + if !strings.Contains(rule, ":") { + nextWorkflowName := rule + total += updatePartBoundsAndSplit(boundedParts, workflowsMap, nextWorkflowName, debugDepth+1) + break + } + + colonSplit := strings.Split(rule, ":") + nextWorkflowName := colonSplit[1] + + if strings.Contains(colonSplit[0], "<") { + conditionalParts := strings.Split(colonSplit[0], "<") + ratingName := conditionalParts[0] + ratingTestValue := cast.ToInt(conditionalParts[1]) + + // fork the part that passes the < conditional + copyOfBounds := copyBoundedPartsMap(boundedParts) + copyOfBounds[ratingName] = [2]int{ + copyOfBounds[ratingName][0], + ratingTestValue - 1, + } + // check that the new bounds are still valid + if copyOfBounds[ratingName][0] <= copyOfBounds[ratingName][1] { + total += updatePartBoundsAndSplit(copyOfBounds, workflowsMap, nextWorkflowName, debugDepth+1) + } + + // second fork for failing the conditional, need to update the boundedParts to fail + boundedParts[ratingName] = [2]int{ + ratingTestValue, + boundedParts[ratingName][1], + } + // check that the new bounds are still valid + if boundedParts[ratingName][0] > boundedParts[ratingName][1] { + break + } + } else if strings.Contains(colonSplit[0], ">") { + conditionalParts := strings.Split(colonSplit[0], ">") + ratingName := conditionalParts[0] + ratingTestValue := cast.ToInt(conditionalParts[1]) + + // fork the part that passes the > conditional + copyOfBounds := copyBoundedPartsMap(boundedParts) + copyOfBounds[ratingName] = [2]int{ + ratingTestValue + 1, + copyOfBounds[ratingName][1], + } + + // check that the new bounds are still valid before recursing + if copyOfBounds[ratingName][0] <= copyOfBounds[ratingName][1] { + total += updatePartBoundsAndSplit(copyOfBounds, workflowsMap, nextWorkflowName, debugDepth+1) + } + + // second fork for failing the conditional, need to update the boundedParts to fail + boundedParts[ratingName] = [2]int{ + boundedParts[ratingName][0], + ratingTestValue, + } + // check that the new bounds are still valid + if boundedParts[ratingName][0] > boundedParts[ratingName][1] { + break + } + } else { + panic("unexpected workflow rule conditional: " + rule) + } + } + + return total +} + +func copyBoundedPartsMap(boundedParts map[string][2]int) map[string][2]int { + cp := map[string][2]int{} + for k, v := range boundedParts { + cp[k] = v + } + return cp +} diff --git a/2023/day19/main_test.go b/2023/day19/main_test.go new file mode 100644 index 0000000..435ae6a --- /dev/null +++ b/2023/day19/main_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "testing" +) + +var example = `px{a<2006:qkq,m>2090:A,rfg} +pv{a>1716:R,A} +lnx{m>1548:A,A} +rfg{s<537:gd,x>2440:R,A} +qs{s>3448:A,lnx} +qkq{x<1416:A,crn} +crn{x>2662:A,R} +in{s<1351:px,qqz} +qqz{s>2770:qs,m<1801:hdj,R} +gd{a>3333:R,R} +hdj{m>838:A,pv} + +{x=787,m=2655,a=1222,s=2876} +{x=1679,m=44,a=2067,s=496} +{x=2036,m=264,a=79,s=2244} +{x=2461,m=1339,a=466,s=291} +{x=2127,m=1623,a=2188,s=1013}` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 19114, + }, + { + name: "actual", + input: input, + want: 287054, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 167409079868000, + }, + { + name: "actual", + input: input, + want: 131619440296497, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day20/main.go b/2023/day20/main.go new file mode 100644 index 0000000..fd88cc1 --- /dev/null +++ b/2023/day20/main.go @@ -0,0 +1,215 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "math" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + ans := pulsePropagation(input, part) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) +} + +func pulsePropagation(input string, part int) int { + modules := parseInput(input) + + var lowPulses, highPulses int + + buttonPresses := 1000 + if part == 2 { + // let this cycle infinitely so I can figure out cycle times for part 2 + buttonPresses = math.MaxInt64 + } + + // for part 2: + // looking at input, rx's only input is &lb, which is a conjunction, so needs to get ALL high signals to send a low to rx + // lb is fed from four other modules that all need to send high signals: + // &rz, &lf, &br, &fk + // figuring out the cycle times of these four then maybe the LCM will be the answer if the input is kind? + + lastCycleForHighPulse := map[string]int{ + "rz": -1, + "lf": -1, + "br": -1, + "fk": -1, + } + + cycles := []int{} + + for i := 0; i < buttonPresses; i++ { + if part == 2 && len(cycles) == 4 { + break + } + + queue := []pulse{} + queue = append(queue, pulse{ + isLowPulse: true, + source: "button", + destination: "broadcaster", + }) + + for len(queue) > 0 { + p := queue[0] + queue = queue[1:] + + if p.isLowPulse { + lowPulses++ + } else { + highPulses++ + } + + if val, ok := lastCycleForHighPulse[p.source]; ok && !p.isLowPulse { + // fmt.Println("found for ", p.source, i+1) + if val == -1 { + lastCycleForHighPulse[p.source] = i + 1 + } else { + cycles = append(cycles, (i+1)-val) + } + } + + if _, ok := modules[p.destination]; !ok { + continue + } + + switch modules[p.destination].moduleType { + case "broadcaster": + for _, dest := range modules[p.destination].destinations { + queue = append(queue, pulse{ + isLowPulse: p.isLowPulse, + source: "broadcaster", + destination: dest, + }) + } + case "flipflop": + if p.isLowPulse { + for _, dest := range modules[p.destination].destinations { + queue = append(queue, pulse{ + // if it was on, it flips off and sends a low pulse + // if it was off, then sends a high pulse (isLowPulse = false) + isLowPulse: modules[p.destination].flipFlopIsOn, + source: p.destination, + destination: dest, + }) + } + // flip it + modules[p.destination].flipFlopIsOn = !modules[p.destination].flipFlopIsOn + } + case "conjunction": + modules[p.destination].conjunctionInputsMapWasLastPulseHigh[p.source] = !p.isLowPulse + allHigh := true + for source, wasStrongPulse := range modules[p.destination].conjunctionInputsMapWasLastPulseHigh { + _ = source + if !wasStrongPulse { + allHigh = false + break + } + } + + for _, dest := range modules[p.destination].destinations { + queue = append(queue, pulse{ + // all high sends a low pulse, otherwise high pulse + isLowPulse: allHigh, + source: p.destination, + destination: dest, + }) + } + default: + panic("unexpected module type" + modules[p.destination].moduleType) + } + } + } + + // wow that worked, super generous on the inputs... + if part == 2 { + ans := 1 + for _, c := range cycles { + ans *= c + } + return ans + } + + return lowPulses * highPulses +} + +type module struct { + moduleType string + name string + flipFlopIsOn bool + conjunctionInputsMapWasLastPulseHigh map[string]bool + destinations []string +} + +type pulse struct { + isLowPulse bool + source, destination string +} + +func parseInput(input string) (ans map[string]*module) { + ans = map[string]*module{} + + for _, line := range strings.Split(input, "\n") { + parts := strings.Split(line, " -> ") + + mod := module{ + moduleType: "", + flipFlopIsOn: false, + conjunctionInputsMapWasLastPulseHigh: map[string]bool{}, + destinations: []string{}, + } + + if parts[0] == "broadcaster" { + mod.moduleType = "broadcaster" + mod.name = "broadcaster" + mod.destinations = strings.Split(parts[1], ", ") + } else if parts[0][:1] == "%" { + mod.moduleType = "flipflop" + mod.name = parts[0][1:] + mod.destinations = strings.Split(parts[1], ", ") + } else if parts[0][:1] == "&" { + mod.moduleType = "conjunction" + mod.name = parts[0][1:] + mod.destinations = strings.Split(parts[1], ", ") + } else { + panic("unidentified module type: " + line) + } + + ans[mod.name] = &mod + } + + // initialize conjunction maps with all their source modules + for name, module := range ans { + for _, dest := range module.destinations { + + if _, ok := ans[dest]; !ok { + continue + } + if ans[dest].moduleType == "conjunction" { + ans[dest].conjunctionInputsMapWasLastPulseHigh[name] = false + } + } + } + + return ans +} diff --git a/2023/day20/main_test.go b/2023/day20/main_test.go new file mode 100644 index 0000000..774fc06 --- /dev/null +++ b/2023/day20/main_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "testing" +) + +var example = `broadcaster -> a, b, c +%a -> b +%b -> c +%c -> inv +&inv -> a` + +var example2 = `broadcaster -> a +%a -> inv, con +&inv -> b +%b -> con +&con -> output` + +func Test_pulsePropagation(t *testing.T) { + tests := []struct { + name string + input string + part int + want int + }{ + { + name: "example", + input: example, + part: 1, + want: 32000000, + }, + { + name: "example2", + input: example2, + part: 1, + want: 11687500, + }, + { + name: "actual", + input: input, + part: 1, + want: 817896682, + }, + { + name: "actual", + input: input, + part: 2, + want: 250924073918341, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pulsePropagation(tt.input, tt.part); got != tt.want { + t.Errorf("pulsePropagation() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day21/main.go b/2023/day21/main.go new file mode 100644 index 0000000..fb5ae2b --- /dev/null +++ b/2023/day21/main.go @@ -0,0 +1,171 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input, 64) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input, 26501365) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string, steps int) int { + grid := parseInput(input) + var row, col int + for r, rowSlice := range grid { + for c, val := range rowSlice { + if val == "S" { + row = r + col = c + break + } + } + } + + queue := map[[2]int]bool{ + {row, col}: true, + } + for i := 0; i < steps; i++ { + newQueue := map[[2]int]bool{} + for coord := range queue { + for _, diff := range [][2]int{ + {-1, 0}, + {1, 0}, + {0, -1}, + {0, 1}, + } { + nextRow := coord[0] + diff[0] + nextCol := coord[1] + diff[1] + if nextRow < 0 || nextRow >= len(grid) || nextCol < 0 || nextCol >= len(grid[0]) { + continue + } + + if grid[nextRow][nextCol] == "." || grid[nextRow][nextCol] == "S" { + newQueue[[2]int{nextRow, nextCol}] = true + } + } + } + + queue = newQueue + } + + return len(queue) +} + +func part2(input string, steps int) int { + grid := parseInput(input) + var row, col int + for r, rowSlice := range grid { + for c, val := range rowSlice { + if val == "S" { + row = r + col = c + break + } + } + } + grid[row][col] = "." + + // keeps track of the flip-flopping coords separately + evenSeenCoords := map[[2]int]bool{} + oddSeenCoords := map[[2]int]bool{} + + // need a set of all coords added to the queue so that we're not re-adding the same coords + uniqueCoords := map[[2]int]bool{} + + queue := [][2]int{ + {row, col}, + } + + // results to calculate quadratic constants with + results := []int{} + + // perform two steps at once to always be on an even number of steps + for s := 0; s < steps && len(results) < 3; s++ { + activeSeenCoords := evenSeenCoords + if s%2 == 1 { + activeSeenCoords = oddSeenCoords + } + + newQueue := [][2]int{} + for _, coord := range queue { + activeSeenCoords[coord] = true + + for _, diff := range [][2]int{ + {-1, 0}, + {1, 0}, + {0, -1}, + {0, 1}, + } { + nextRow := coord[0] + diff[0] + nextCol := coord[1] + diff[1] + nextCoord := [2]int{nextRow, nextCol} + + // handles infinite grid and garden space detection + modNextRow := ((nextRow % len(grid)) + len(grid)) % +len(grid) + modNextCol := ((nextCol % len(grid[0])) + len(grid[0])) % len(grid[0]) + if grid[modNextRow][modNextCol] != "." { + continue + } + + // if already seen, skip + if uniqueCoords[nextCoord] { + continue + } + uniqueCoords[nextCoord] = true + + newQueue = append(newQueue, nextCoord) + } + } + + queue = newQueue + + if s != 0 && s%131 == 65 { + results = append(results, len(activeSeenCoords)) + } + } + + // solve quadratic for a b and c constants + a := (results[2] + results[0] - 2*results[1]) / 2 + b := results[1] - results[0] - a + c := results[0] + + n := steps / len(grid) + + return a*n*n + b*n + c +} + +func parseInput(input string) (ans [][]string) { + for _, line := range strings.Split(input, "\n") { + ans = append(ans, strings.Split(line, "")) + } + return ans +} diff --git a/2023/day21/main_test.go b/2023/day21/main_test.go new file mode 100644 index 0000000..1fd6b0c --- /dev/null +++ b/2023/day21/main_test.go @@ -0,0 +1,93 @@ +package main + +import ( + "testing" +) + +var example = `........... +.....###.#. +.###.##..#. +..#.#...#.. +....#.#.... +.##..S####. +.##..#...#. +.......##.. +.##.#.####. +.##..##.##. +...........` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + steps int + want int + }{ + { + name: "example", + input: example, + steps: 6, + want: 16, + }, + { + name: "actual", + input: input, + steps: 64, + want: 3743, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input, tt.steps); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + steps int + want int + }{ + // { + // name: "example-10", + // input: example, + // steps: 10, + // want: 50, + // }, + // { + // name: "example-50", + // input: example, + // steps: 50, + // want: 1594, + // }, + // { + // name: "example-100", + // input: example, + // steps: 100, + // want: 6536, + // }, + // { + // name: "example-5k", + // input: example, + // steps: 5000, + // want: 16733044, + // }, + { + name: "actual", + input: input, + steps: 26501365, + want: 618261433219147, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input, tt.steps); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day22/main.go b/2023/day22/main.go new file mode 100644 index 0000000..58e1b77 --- /dev/null +++ b/2023/day22/main.go @@ -0,0 +1,223 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/algos" + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + bricks := parseInput(input) + + dropBricks(bricks) + + // basically have a graph... + // if a brick supports nothing, it can be removed + // if a brick supports + removableBricks := map[int]bool{} + for _, brick := range bricks { + if len(brick.supports) == 0 { + // supports nothing + removableBricks[brick.index] = true + } else { + // check ALL supported bricks, if this is the ONLY support for those + // then this brick CANNOT be removed + hasUniqueDependency := false + for supportedBrickIndex := range brick.supports { + if len(bricks[supportedBrickIndex].supportedBy) == 1 { + hasUniqueDependency = true + } + } + if !hasUniqueDependency { + removableBricks[brick.index] = true + } + } + } + + return len(removableBricks) +} + +func dropBricks(bricks []*brick) { + // process bricks by lowest z values + sort.Slice(bricks, func(i, j int) bool { + return bricks[i].start[2] < bricks[j].start[2] + }) + + // re-index, pesky bug... + for i, brick := range bricks { + brick.index = i + } + + // all bricks are < 10 units of volume, so can store their coords in a map... + // also all bricks in the input are straight lines, only one dimension will be > 1 + occupiedCells := map[[3]int]int{} + for _, brick := range bricks { + + isBlocked := false + for brick.start[2] > 1 && !isBlocked { + for _, coord := range brick.coords { + downOne := [3]int{coord[0], coord[1], coord[2] - 1} + if index, ok := occupiedCells[downOne]; ok { + isBlocked = true + + brick.supportedBy[index] = true + bricks[index].supports[brick.index] = true + } + } + + if !isBlocked { + for i := range brick.coords { + brick.coords[i][2]-- + } + brick.start[2]-- + brick.end[2]-- + } + } + + for _, coord := range brick.coords { + occupiedCells[coord] = brick.index + } + } +} + +func part2(input string) int { + bricks := parseInput(input) + dropBricks(bricks) + + total := 0 + + // chain reaction + for i := range bricks { + bricksCopy := copyAllBricks(bricks) + startingBrick := bricksCopy[i] + + queueToRemove := []*brick{} + for in := range startingBrick.supports { + delete(bricksCopy[in].supportedBy, startingBrick.index) + queueToRemove = append(queueToRemove, bricksCopy[in]) + } + + removed := 0 + for len(queueToRemove) > 0 { + br := queueToRemove[0] + queueToRemove = queueToRemove[1:] + + if len(br.supportedBy) > 0 { + continue + } + + removed++ + + // check every brick it supports, remove self from it's supportedBy map + // then add to queue to be checked + for supportedBrickIndex := range br.supports { + delete(bricksCopy[supportedBrickIndex].supportedBy, br.index) + if len(bricksCopy[supportedBrickIndex].supportedBy) == 0 { + queueToRemove = append(queueToRemove, bricksCopy[supportedBrickIndex]) + } + } + } + + total += removed + } + + return total +} + +func copyAllBricks(bricks []*brick) []*brick { + copiedBricks := []*brick{} + for _, b := range bricks { + newBrick := &brick{ + // start: []int{}, + // end: []int{}, + index: b.index, + // coords: [][3]int{}, + supportedBy: map[int]bool{}, + supports: map[int]bool{}, + } + // need full copies of these otherwise they'll point to the same underlying maps + for k, v := range b.supportedBy { + newBrick.supportedBy[k] = v + } + for k, v := range b.supports { + newBrick.supports[k] = v + } + copiedBricks = append(copiedBricks, newBrick) + } + return copiedBricks +} + +type brick struct { + start, end []int + index int + coords [][3]int + supportedBy map[int]bool + supports map[int]bool +} + +func parseInput(input string) (ans []*brick) { + for _, line := range strings.Split(input, "\n") { + coords := [6]int{} + for i, part := range algos.SplitStringOn(line, []string{",", "~"}) { + coords[i] = cast.ToInt(part) + } + + if coords[0] > coords[3] || coords[1] > coords[4] || coords[2] > coords[5] { + panic("unordered input") + } + + allCoords := [][3]int{} + for x := coords[0]; x <= coords[3]; x++ { + for y := coords[1]; y <= coords[4]; y++ { + for z := coords[2]; z <= coords[5]; z++ { + allCoords = append(allCoords, [3]int{x, y, z}) + } + } + } + + ans = append(ans, &brick{ + start: coords[:3], + end: coords[3:], + index: len(ans), + coords: allCoords, + supportedBy: map[int]bool{}, + supports: map[int]bool{}, + }) + } + + return ans +} diff --git a/2023/day22/main_test.go b/2023/day22/main_test.go new file mode 100644 index 0000000..8afd636 --- /dev/null +++ b/2023/day22/main_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "testing" +) + +var example = `1,0,1~1,2,1 +0,0,2~2,0,2 +0,2,3~2,2,3 +0,0,4~0,2,4 +2,0,5~2,2,5 +0,1,6~2,1,6 +1,1,8~1,1,9` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 5, + }, + { + name: "actual", + input: input, + want: 471, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 7, + }, + { + name: "actual", + input: input, + want: 68525, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day23/main.go b/2023/day23/main.go new file mode 100644 index 0000000..584e727 --- /dev/null +++ b/2023/day23/main.go @@ -0,0 +1,217 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + grid := parseInput(input) + + // do not step on same tile twice, longest hike possible + // standard backtrack? + var startCol int + for c := 0; c < len(grid[0]); c++ { + if grid[0][c] == "." { + startCol = c + break + } + } + + return backtrackLongest(grid, 0, startCol, map[[2]int]bool{}, 0) +} + +var slopes = map[string][2]int{ + ">": {0, 1}, + "<": {0, -1}, + "v": {1, 0}, + "^": {-1, 0}, +} + +type node struct { + row, col int + weightedEdges map[*node]int +} + +func backtrackLongest(grid [][]string, row, col int, visited map[[2]int]bool, steps int) int { + if row == len(grid)-1 && grid[row][col] == "." { + return steps + } + + if diff, ok := slopes[grid[row][col]]; ok { + + nextCoord := [2]int{row + diff[0], col + diff[1]} + if visited[nextCoord] { + return 0 + } + + visited[[2]int{row, col}] = true + + result := backtrackLongest(grid, row+diff[0], col+diff[1], visited, steps+1) + + visited[[2]int{row, col}] = false + return result + } + + best := 0 + + for _, diff := range slopes { + nextRow := row + diff[0] + nextCol := col + diff[1] + + if nextRow < 0 || nextRow >= len(grid) || + nextCol < 0 || nextCol >= len(grid[0]) { + continue + } + + nextCoord := [2]int{nextRow, nextCol} + + if visited[nextCoord] { + continue + } + + if grid[nextRow][nextCol] != "#" { + visited[[2]int{row, col}] = true + + result := backtrackLongest(grid, nextRow, nextCol, visited, steps+1) + best = max(best, result) + + visited[[2]int{row, col}] = false + } + } + + return best +} + +func part2(input string) int { + grid := parseInput(input) + + var startCol int + for c := 0; c < len(grid[0]); c++ { + if grid[0][c] == "." { + startCol = c + break + } + } + _ = startCol + // reduce to a graph with weighted edges + allNodes := map[[2]int]*node{} + + // just make all nodes + for r := 0; r < len(grid); r++ { + for c := 0; c < len(grid[0]); c++ { + if grid[r][c] == "#" { + continue + } + allNodes[[2]int{r, c}] = &node{ + row: r, + col: c, + weightedEdges: map[*node]int{}, + } + + } + } + + // connect all adjacent nodes and assign a weight of 1 + for coords, node := range allNodes { + for _, diff := range slopes { + nextCoord := [2]int{ + coords[0] + diff[0], + coords[1] + diff[1], + } + + if neighbor, ok := allNodes[nextCoord]; ok { + node.weightedEdges[neighbor] = 1 + neighbor.weightedEdges[node] = 1 + } + } + } + + // reduce the graph by combining neighbors if there are exactly two + for _, currentNode := range allNodes { + if len(currentNode.weightedEdges) == 2 { + twoNeighbors := []*node{} + summedWeight := 0 + for neighborNode := range currentNode.weightedEdges { + twoNeighbors = append(twoNeighbors, neighborNode) + summedWeight += neighborNode.weightedEdges[currentNode] + } + + delete(twoNeighbors[0].weightedEdges, currentNode) + delete(twoNeighbors[1].weightedEdges, currentNode) + twoNeighbors[0].weightedEdges[twoNeighbors[1]] = summedWeight + twoNeighbors[1].weightedEdges[twoNeighbors[0]] = summedWeight + + // doesn't affect map iteration + delete(allNodes, [2]int{currentNode.row, currentNode.col}) + } + } + + // backtrack through graph again + return backtrackThroughGraph(allNodes[[2]int{0, startCol}], + map[*node]bool{}, 0, len(grid)-1) +} + +func backtrackThroughGraph(currentNode *node, seen map[*node]bool, + distance int, destinationRow int) int { + + // destination row is knowing that there is only one node that is on the + // final row, so if we reach that depth we've reached the end + if currentNode.row == destinationRow { + return distance + } + + best := 0 + seen[currentNode] = true + + for neighbor, weight := range currentNode.weightedEdges { + if seen[neighbor] { + continue + } + best = max(best, + backtrackThroughGraph(neighbor, seen, distance+weight, destinationRow)) + } + + seen[currentNode] = false + + return best +} + +func parseInput(input string) (ans [][]string) { + for _, line := range strings.Split(input, "\n") { + ans = append(ans, strings.Split(line, "")) + } + return ans +} diff --git a/2023/day23/main_test.go b/2023/day23/main_test.go new file mode 100644 index 0000000..f3524c3 --- /dev/null +++ b/2023/day23/main_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "testing" +) + +var example = `#.##################### +#.......#########...### +#######.#########.#.### +###.....#.>.>.###.#.### +###v#####.#v#.###.#.### +###.>...#.#.#.....#...# +###v###.#.#.#########.# +###...#.#.#.......#...# +#####.#.#.#######.#.### +#.....#.#.#.......#...# +#.#####.#.#.#########v# +#.#...#...#...###...>.# +#.#.#v#######v###.###v# +#...#.>.#...>.>.#.###.# +#####v#.#.###v#.#.###.# +#.....#...#...#.#.#...# +#.#########.###.#.#.### +#...###...#...#...#.### +###.###.#.###v#####v### +#...#...#.#.>.>.#.>.### +#.###.###.#.###.#.#v### +#.....###...###...#...# +#####################.#` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 94, + }, + { + name: "actual", + input: input, + want: 2294, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 154, + }, + { + name: "actual", + input: input, + want: 6418, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day24/main.go b/2023/day24/main.go new file mode 100644 index 0000000..4c4bdae --- /dev/null +++ b/2023/day24/main.go @@ -0,0 +1,232 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input, [2]float64{200000000000000, 400000000000000}) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string, testRange [2]float64) int { + hailstones := parseInput(input) + + ans := 0 + + // only move forward (per the velocity) + for i, hs1 := range hailstones { + for _, hs2 := range hailstones[i+1:] { + intersection := getIntersectingCoordinates(hs1, hs2) + if intersection == nil { + continue + } + + // ensure intersection is in the right direction + // solve for time to reach intersection? + if solveForTimeToReachPoint(hs1, intersection) < 0 || solveForTimeToReachPoint(hs2, intersection) < 0 { + continue + } + + if testRange[0] <= intersection[0] && intersection[0] <= testRange[1] && + testRange[0] <= intersection[1] && intersection[1] <= testRange[1] { + + ans++ + } + } + } + + return ans +} + +// does not check for if the lines are the same line +func areHailstonesParallel(hs1, hs2 hailstone) bool { + if hs1.hasVerticalPath && hs2.hasVerticalPath { + return true + } + if hs1.hasVerticalPath || hs2.hasVerticalPath { + return false + } + + return hs1.slope == hs2.slope +} + +// returns nil slice if the lines do not intersect +func getIntersectingCoordinates(hs1, hs2 hailstone) []float64 { + if areHailstonesParallel(hs1, hs2) { + return nil + } + // assume not the exact same line and that there is only one intersection point + + // point-slope line formula + // y - y1 = m(x - x1) + x := (hs1.slope*hs1.x - hs2.slope*hs2.x + hs2.y - hs1.y) / (hs1.slope - hs2.slope) + y := hs1.slope*(x-hs1.x) + hs1.y + + return []float64{x, y} +} + +func solveForTimeToReachPoint(hs hailstone, point []float64) float64 { + if len(point) != 2 { + panic("expected len == 2 for point slice") + } + // x = vx * t + x0 + // t = (intersection_x - x_0) / vx + t := (point[0] - hs.x) / hs.vx + + return t +} + +func part2(input string) int { + hailstones := parseInput(input) + + var possibleRockVelX, possibleRockVelY, possibleRockVelZ []int + for i, hs1 := range hailstones { + for _, hs2 := range hailstones[i+1:] { + + if hs1.vx == hs2.vx { + possibilities := getPossibleVelocities(int(hs2.x), int(hs1.x), int(hs1.vx)) + if len(possibleRockVelX) == 0 { + possibleRockVelX = possibilities + } else { + possibleRockVelX = getIntersection(possibleRockVelX, possibilities) + } + } + if hs1.vy == hs2.vy { + possibilities := getPossibleVelocities(int(hs2.y), int(hs1.y), int(hs1.vy)) + if len(possibleRockVelY) == 0 { + possibleRockVelY = possibilities + } else { + possibleRockVelY = getIntersection(possibleRockVelY, possibilities) + } + } + if hs1.vz == hs2.vz { + possibilities := getPossibleVelocities(int(hs2.z), int(hs1.z), int(hs1.vz)) + if len(possibleRockVelZ) == 0 { + possibleRockVelZ = possibilities + } else { + possibleRockVelZ = getIntersection(possibleRockVelZ, possibilities) + } + } + } + } + + if len(possibleRockVelX) == 1 && len(possibleRockVelY) == 1 && len(possibleRockVelZ) == 1 { + rockVelX := float64(possibleRockVelX[0]) + rockVelY := float64(possibleRockVelY[0]) + rockVelZ := float64(possibleRockVelZ[0]) + + hailstoneA, hailstoneB := hailstones[0], hailstones[1] + mA := (hailstoneA.vy - rockVelY) / (hailstoneA.vx - rockVelX) + mB := (hailstoneB.vy - rockVelY) / (hailstoneB.vx - rockVelX) + cA := hailstoneA.y - (mA * hailstoneA.x) + cB := hailstoneB.y - (mB * hailstoneB.x) + rockX := (cB - cA) / (mA - mB) + rockY := mA*rockX + cA + time := (rockX - hailstoneA.x) / (hailstoneA.vx - rockVelX) + rockZ := hailstoneA.z + (hailstoneA.vz-rockVelZ)*time + return int(rockX + rockY + rockZ) + } + + panic("more than one possible velocity in a direction") +} + +func getPossibleVelocities(pos1, pos2 int, vel int) []int { + match := []int{} + for possibleVel := -1000; possibleVel < 1000; possibleVel++ { + if possibleVel != vel && (pos1-pos2)%(possibleVel-vel) == 0 { + match = append(match, possibleVel) + } + } + return match +} + +func getIntersection(sli1, sli2 []int) []int { + result := []int{} + + map2 := map[int]bool{} + for _, val := range sli2 { + map2[val] = true + } + + for _, val := range sli1 { + if map2[val] { + result = append(result, val) + } + } + return result +} + +func parseInput(input string) (ans []hailstone) { + for _, line := range strings.Split(input, "\n") { + positions := []float64{} + vels := []float64{} + + line = strings.ReplaceAll(line, ",", "") + parts := strings.Split(line, " @ ") + + for _, posStr := range strings.Fields(parts[0]) { + positions = append(positions, float64(cast.ToInt(posStr))) + } + for _, velStr := range strings.Fields(parts[1]) { + vels = append(vels, float64(cast.ToInt(velStr))) + } + + ans = append(ans, makeHailstone(positions[0], positions[1], positions[2], vels[0], vels[1], vels[2])) + } + + return ans +} + +type hailstone struct { + x, y, z float64 + vx, vy, vz float64 + hasVerticalPath bool + slope float64 +} + +func makeHailstone(x, y, z, vx, vy, vz float64) hailstone { + hs := hailstone{ + x: x, + y: y, + z: z, + vx: vx, + vy: vy, + vz: vz, + hasVerticalPath: vx == 0, + slope: 0, + } + if !hs.hasVerticalPath { + hs.slope = vy / vx + } + return hs +} diff --git a/2023/day24/main_test.go b/2023/day24/main_test.go new file mode 100644 index 0000000..5b9ffeb --- /dev/null +++ b/2023/day24/main_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "testing" +) + +var example = `19, 13, 30 @ -2, 1, -2 +18, 19, 22 @ -1, -1, -2 +20, 25, 34 @ -2, -2, -4 +12, 31, 28 @ -1, -2, -1 +20, 19, 15 @ 1, -5, -3` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + testRange [2]float64 + want int + }{ + { + name: "example", + input: example, + testRange: [2]float64{7, 27}, + want: 2, + }, + { + name: "actual", + input: input, + testRange: [2]float64{200000000000000, 400000000000000}, + want: 31921, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input, tt.testRange); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + // example input is not big enough for this logic to work + // { + // name: "example", + // input: example, + // want: 47, + // }, + { + name: "actual", + input: input, + want: 761691907059631, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day25/main.go b/2023/day25/main.go new file mode 100644 index 0000000..706156f --- /dev/null +++ b/2023/day25/main.go @@ -0,0 +1,226 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "math/rand" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + graph := parseInput(input) + + // if brute forcing, can check if the separated nodes are able to traverse to each other + // which would indicate that it has broken an internal (to a group) edge, and not separated + // the overall group... + + // brute forcing 3k edges for my input with 3 traversals for each... + // for n = total edges + // O(n^4), triple nested for loop with traversal check in each + // O(3k^4) = 81,000,000,000,000 way too slow + + // strategy actually used: + // pick 200 random pairs of nodes, traverse between the two of them + // the most traversed nodes will likely be the bridges + // a similar strategy would be to BFS traverse from a few randomly selected nodes to the furthest + // possible node, the furthest depth away is likely in the other group (assuming a kind input) + // and therefore those paths can be used to tabulate the most trafficked edges aka 3 bridges + var allNodes []string + for n := range graph { + allNodes = append(allNodes, n) + } + + // for a small (example) graph just pick every node + // for a large (actual input) graph, pick 200 nodes + pairsToPick := min(200, len(graph)*(len(graph)-1)) + + traversedPairs := map[string]bool{} + timesEdgeTraversed := map[string]int{} + + for len(traversedPairs) < pairsToPick { + i1 := rand.Intn(len(allNodes)) + i2 := rand.Intn(len(allNodes)) + if i1 == i2 { + continue + } + + n1 := allNodes[i1] + n2 := allNodes[i2] + + randomPairName := sortedEdgeName(n1, n2) + + if traversedPairs[randomPairName] { + continue + } + traversedPairs[randomPairName] = true + + path := findShortestPath(graph, n1, n2) + + for i := 1; i < len(path); i++ { + timesEdgeTraversed[sortedEdgeName(path[i-1], path[i])]++ + } + } + + threeMostTraffickedEdges := getThreeMostTraffickedEdges(timesEdgeTraversed) + + // remove edge + for _, edge := range threeMostTraffickedEdges { + nodes := strings.Split(edge, " ") + + graph[nodes[0]] = removeElementFromSlice(graph[nodes[0]], nodes[1]) + graph[nodes[1]] = removeElementFromSlice(graph[nodes[1]], nodes[0]) + } + + sizes := []int{} + for _, node := range strings.Split(threeMostTraffickedEdges[0], " ") { + sizes = append(sizes, getGroupSize(graph, node, map[string]bool{})) + } + + if len(sizes) != 2 { + panic("expected two groups") + } + + return sizes[0] * sizes[1] +} + +func sortedEdgeName(node1, node2 string) string { + lower := min(node1, node2) + higher := max(node1, node2) + return fmt.Sprintf("%v %v", lower, higher) +} + +func findShortestPath(graph map[string][]string, start, end string) []string { + type dfsNode struct { + current string + pathSoFar []string + } + seen := map[string]bool{} + queue := []dfsNode{ + { + current: start, + pathSoFar: []string{start}, + }, + } + + for len(queue) > 0 { + popped := queue[0] + queue = queue[1:] + if seen[popped.current] { + continue + } + seen[popped.current] = true + + if popped.current == end { + return popped.pathSoFar + } + + for _, neighbor := range graph[popped.current] { + if seen[neighbor] { + continue + } + nextNode := dfsNode{ + current: neighbor, + pathSoFar: append([]string{}, popped.pathSoFar...), // deep copy + } + nextNode.pathSoFar = append(nextNode.pathSoFar, neighbor) + + queue = append(queue, nextNode) + } + } + + panic("expect return from loop") +} + +func getThreeMostTraffickedEdges(timesEdgeTraversed map[string]int) []string { + ans := []string{} + + for len(ans) < 3 { + var bestEdge string + var bestCount int + + for edge, count := range timesEdgeTraversed { + if count > bestCount { + bestCount = count + bestEdge = edge + } + } + + ans = append(ans, bestEdge) + delete(timesEdgeTraversed, bestEdge) + } + + return ans +} + +func removeElementFromSlice(sli []string, ele string) []string { + for i, n := range sli { + if n == ele { + sli[len(sli)-1], sli[i] = sli[i], sli[len(sli)-1] + sli = sli[:len(sli)-1] + + return sli + } + } + panic("element not found") +} + +func getGroupSize(graph map[string][]string, node string, seen map[string]bool) int { + if seen[node] { + return 0 + } + size := 1 + seen[node] = true + for _, neighbor := range graph[node] { + size += getGroupSize(graph, neighbor, seen) + } + return size +} + +func part2(input string) string { + return "happiness" +} + +func parseInput(input string) (graph map[string][]string) { + graph = map[string][]string{} + + for _, line := range strings.Split(input, "\n") { + parts := strings.Split(line, ": ") + for _, node := range strings.Split(parts[1], " ") { + graph[parts[0]] = append(graph[parts[0]], node) + graph[node] = append(graph[node], parts[0]) + } + } + + return graph +} diff --git a/2023/day25/main_test.go b/2023/day25/main_test.go new file mode 100644 index 0000000..ecb286a --- /dev/null +++ b/2023/day25/main_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "testing" +) + +var example = `jqt: rhn xhk nvd +rsh: frs pzl lsr +xhk: hfx +cmg: qnr nvd lhk bvb +rhn: xhk bvb hfx +bvb: xhk hfx +pzl: lsr hfx nvd +qnr: nvd +ntq: jqt hfx bvb xhk +nvd: lhk +lsr: lhk +rzs: qnr cmg lsr rsh +frs: qnr lhk lsr` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 54, + }, + { + name: "actual", + input: input, + want: 567606, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "example", + input: example, + want: "happiness", + }, + { + name: "actual", + input: input, + want: "happiness", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2024/day01/main.go b/2024/day01/main.go new file mode 100644 index 0000000..b01efeb --- /dev/null +++ b/2024/day01/main.go @@ -0,0 +1,78 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + list1, list2 := parseInput(input) + sort.Ints(list1) + sort.Ints(list2) + + ans := 0 + for i := range len(list1) { + ans += mathy.AbsInt(list2[i] - list1[i]) + } + + return ans +} + +func part2(input string) int { + list1, list2 := parseInput(input) + + countsList2 := map[int]int{} + for _, v := range list2 { + countsList2[v]++ + } + + ans := 0 + for _, v := range list1 { + ans += v * countsList2[v] + } + return ans +} + +func parseInput(input string) (list1, list2 []int) { + for _, line := range strings.Split(input, "\n") { + nums := strings.Split(line, " ") + list1 = append(list1, cast.ToInt(nums[0])) + list2 = append(list2, cast.ToInt(nums[1])) + } + return list1, list2 +} diff --git a/2024/day01/main_test.go b/2024/day01/main_test.go new file mode 100644 index 0000000..e5c1bc1 --- /dev/null +++ b/2024/day01/main_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "testing" +) + +var example = `3 4 +4 3 +2 5 +1 3 +3 9 +3 3` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 11, + }, + { + name: "actual", + input: input, + want: 1651298, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 31, + }, + { + name: "actual", + input: input, + want: 21306195, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2024/day02/day02.py b/2024/day02/day02.py new file mode 100644 index 0000000..bade04c --- /dev/null +++ b/2024/day02/day02.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +from typing import List + +example = """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""" + + +def part1(grid: List[List[int]]) -> int: + valid_levels = 0 + for level in grid: + if test_level(level): + valid_levels += 1 + return valid_levels + + +def part2(grid: List[List[int]]) -> int: + # tolerate one bad level... + valid_levels: int = 0 + for level in grid: + for i in range(len(level)): + newLevel: List[int] = level.copy() + newLevel.pop(i) # removes i-th element?, how convenient.. + if test_level(newLevel): + valid_levels += 1 + break + + return valid_levels + + +def test_level(level: List[int]) -> bool: + is_increasing = level[1] > level[0] + is_valid = True + + # The levels are either all increasing or all decreasing. + # Any two adjacent levels differ by at least one and at most three. + for i in range(1, len(level)): + if is_increasing and level[i] <= level[i - 1]: + return False + elif not is_increasing and level[i] >= level[i - 1]: + return False + + diff = abs(level[i] - level[i - 1]) + if diff < 1 or diff > 3: + return False + + return is_valid + + +def convert_input(input: str) -> List[List[int]]: + grid: List[List[int]] = [] + + for level in input.splitlines(): + grid.append([int(part) for part in level.split(" ")]) + # converted_level: List[int] = [] + # this syntax is going to take getting used to... + # for part in level.split(" "): + # converted_level.append(int(part)) + # grid.append(converted_level) + + return grid + + +example_grid = convert_input(example) +print("example part1:", part1(example_grid), "want", 2) + +input = open("input.txt", "r").read() +grid = convert_input(input) +print("part1:", part1(grid), "want", 585) + +print("example part2:", part2(example_grid), "want", 4) +print("part2:", part2(grid), "want", 626) diff --git a/2024/day02/main.go b/2024/day02/main.go new file mode 100644 index 0000000..58372b7 --- /dev/null +++ b/2024/day02/main.go @@ -0,0 +1,106 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + grid := convertInput(input) + validLevels := 0 + for _, level := range grid { + if testLevel(level) { + validLevels += 1 + } + } + return validLevels +} + +func part2(input string) int { + grid := convertInput(input) + // tolerate one bad level... + validLevels := 0 + for _, level := range grid { + for i := range len(level) { + newLevel := []int{} + for j := range len(level) { + if i != j { + newLevel = append(newLevel, level[j]) + } + } + if testLevel(newLevel) { + validLevels += 1 + break + } + } + } + + return validLevels +} + +func testLevel(level []int) bool { + isIncreasing := level[1] > level[0] + + // The levels are either all increasing or all decreasing. + // Any two adjacent levels differ by at least one and at most three. + for i := 1; i < len(level); i++ { + if isIncreasing && level[i] <= level[i-1] { + return false + } else if !isIncreasing && level[i] >= level[i-1] { + return false + } + + diff := mathy.AbsInt(level[i] - level[i-1]) + if diff < 1 || diff > 3 { + return false + } + } + + return true +} + +func convertInput(input string) [][]int { + grid := [][]int{} + for _, line := range strings.Split(input, "\n") { + level := []int{} + for _, n := range strings.Split(line, " ") { + level = append(level, cast.ToInt(n)) + } + grid = append(grid, level) + } + return grid +} diff --git a/2024/day02/main_test.go b/2024/day02/main_test.go new file mode 100644 index 0000000..c61bc01 --- /dev/null +++ b/2024/day02/main_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "testing" +) + +var example = `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` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 2, + }, + { + name: "actual", + input: input, + want: 585, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 4, + }, + { + name: "actual", + input: input, + want: 626, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2024/day03/day03.py b/2024/day03/day03.py new file mode 100644 index 0000000..625cf27 --- /dev/null +++ b/2024/day03/day03.py @@ -0,0 +1,43 @@ +import re + + +def part1(input: str) -> int: + matches = re.findall(r"mul\(\d+,\d+\)", input) + ans = 0 + for match in matches: + ans += exec_mul(match) + return ans + + +def part2(input: str) -> int: + matches = re.findall(r"(do\(\)|don\'t\(\)|mul\(\d+,\d+\))", input) + do = True + ans = 0 + + for match in matches: + if match == "do()": + do = True + elif match == "don't()": + do = False + elif do: + ans += exec_mul(match) + + return ans + + +def exec_mul(s: str) -> int: + nums = re.findall(r"\d+", s) + return int(nums[0]) * int(nums[1]) + + +example = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))" + +print("example part1:", part1(example), "want", 161) + +input = open("input.txt").read() +print("part1:", part1(input), "want", 169021493) + +example2 = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))" + +print("example part2:", part2(example2), "want", 48) +print("part2:", part2(input), "want", 111762583) diff --git a/2024/day04/day04.py b/2024/day04/day04.py new file mode 100644 index 0000000..5b7de2e --- /dev/null +++ b/2024/day04/day04.py @@ -0,0 +1,80 @@ +from typing import List, Tuple + + +example = """MMMSXXMASM +MSAMXMSMSA +AMXSXMAAMM +MSAMASMSMX +XMASAMXAMM +XXAMMXXAMA +SMSMSASXSS +SAXAMASAAA +MAMMMXMMMM +MXMXAXMASX""" + + +def part1(input: str) -> int: + grid: List[str] = input.splitlines() + + ans = 0 + + # 8 directions that words can be found in once a "X" is found + dirs: List[Tuple[int, int]] = [ + (-1, -1), + (-1, 0), + (-1, 1), + (0, -1), + (0, 1), + (1, -1), + (1, 0), + (1, 1), + ] + + for i in range(len(grid)): + for j in range(len(grid[0])): + if grid[i][j] == "X": + for dir in dirs: + if getWord(grid, i, j, dir) == "XMAS": + ans += 1 + return ans + + +def getWord(grid: List[str], row: int, col: int, dir: Tuple[int, int]) -> str: + word = grid[row][col] + while len(word) < 4: + row += dir[0] + col += dir[1] + if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]): + return "" + word += grid[row][col] + + return word + + +def part2(input: str) -> int: + grid: List[str] = input.splitlines() + + ans: int = 0 + + # do not search outer border of the grid because we're looking for A's which + # will only be "valid" if they are not on the outer border + for i in range(1, len(grid) - 1): + for j in range(1, len(grid[0]) - 1): + if grid[i][j] == "A": + backslash_word = grid[i - 1][j - 1] + grid[i + 1][j + 1] + slash_word = grid[i - 1][j + 1] + grid[i + 1][j - 1] + + backslash_valid = backslash_word == "MS" or backslash_word == "SM" + slash_valid = slash_word == "MS" or slash_word == "SM" + if backslash_valid and slash_valid: + ans += 1 + return ans + + +input = open("input.txt", "r").read() + +print(f"part1 example: {part1(example)} want 18") +print(f"part1: {part1(input)} want 2468") + +print(f"part2 example: {part2(example)} want 9") +print(f"part2: {part2(input)} want 1864") diff --git a/2024/day05/day05.py b/2024/day05/day05.py new file mode 100644 index 0000000..260d01b --- /dev/null +++ b/2024/day05/day05.py @@ -0,0 +1,120 @@ +from collections import defaultdict + + +def day5(input: str, part: int) -> int: + split_input = input.split("\n\n") + + reversed_graph: defaultdict[int, list[int]] = defaultdict(list) + for line in split_input[0].splitlines(): + rule_parts = line.split("|") + X, Y = int(rule_parts[0]), int(rule_parts[1]) + reversed_graph[Y].append(X) + + updates: list[list[int]] = [ + [int(x) for x in line.split(",")] for line in split_input[1].splitlines() + ] + part1_ans: int = 0 + + # for part 2 + invalid_updates: list[list[int]] = [] + + for update in updates: + # assuming there are no repeat updates... + seen_set: set[int] = set() + disallowed_set: set[int] = set() + is_valid = True + + for num in update: + if num in disallowed_set: + is_valid = False + break + for cannot_come_before in reversed_graph[num]: + if not cannot_come_before in seen_set: + disallowed_set.add(cannot_come_before) + + seen_set.add(num) + + if is_valid: + part1_ans += update[len(update) // 2] + else: + invalid_updates.append(update) + if part == 1: + return part1_ans + + # part2 + # can assume there's only one valid order... or that unordered ones will not affect the middle value + # so just take all the numbers and create the correct order by traversing the graph of dependencies? + + part2_ans: int = 0 + for update in invalid_updates: + correct_order: list[int] = [] + all_nums: set[int] = set(update) + used_nums: set[int] = set() + while len(all_nums) > len(used_nums): + for num in all_nums: + if num in used_nums: + continue + if not num in reversed_graph: + correct_order.append(num) + # all_nums.remove(num) + used_nums.add(num) + continue + + # check if all of this num's dependencies are in used_nums + # or if its dependencies are not present at all + # there's definitely a better algo for this... + if all( + [ + dep in used_nums or not dep in all_nums + for dep in reversed_graph[num] + ] + ): + correct_order.append(num) + # all_nums.remove(num) + used_nums.add(num) + continue + part2_ans += correct_order[len(correct_order) // 2] + + return part2_ans + + +# page ordering rules, X|Y -> X must come before Y +# updates, basically orders of pages +# missing page numbers are ignored.. +# part1_ans: sum middle page of each valid update +example = """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""" + +input = open("input.txt").read() + +print(f"day5 example: {day5(example,1)} want 143") +print(f"day5: {day5(input,1)} want 5166") + +print(f"part2 example: {day5(example,2)} want 123") +print(f"part2: {day5(input,2)} want 4679") diff --git a/2024/day06/day06.py b/2024/day06/day06.py new file mode 100644 index 0000000..1e23390 --- /dev/null +++ b/2024/day06/day06.py @@ -0,0 +1,165 @@ +# guard walks forward, turns right if blocked, turn or step = 1 move + + +import time + + +def part1(input: str) -> int: + return len(get_full_path(input)) + + +# changed this to return the set of all coords on the path to (slightly) speed up part 2 +# so part 2 can just modify things already on the guard's path instead of brute forcing +# the entire grid +def get_full_path(input: str) -> set[tuple[int, int]]: + lines = input.splitlines() + + row: int = 0 + col: int = 0 + + # find starting row and col + for r in range(len(lines)): + for c in range(len(lines[0])): + if lines[r][c] == "^": + row = r + col = c + break + + # example and my input both have the guard just starting pointing north + dir_index: int = 0 + dirs: list[list[int]] = [ + [-1, 0], + [0, 1], + [1, 0], + [0, -1], + ] + + seen: set[tuple[int, int]] = set() + + # assume that we just have to run off the grid?.. + while row >= 0 and row < len(lines) and col >= 0 and col < len(lines[0]): + coord = (row, col) + + seen.add(coord) + dir = dirs[dir_index] + nextRow: int = row + dir[0] + nextCol: int = col + dir[1] + if not ( + nextRow >= 0 + and nextRow < len(lines) + and nextCol >= 0 + and nextCol < len(lines[0]) + ): + break + if lines[nextRow][nextCol] == "#": + # rotate + dir_index += 1 + dir_index %= 4 + else: + row = nextRow + col = nextCol + + # return len(seen) + return seen + + +def part2(input: str) -> int: + + path = get_full_path(input) + + # list(str) divides the str into individual characters? + # can't just use str.split("") + # need an actual 2D array so we can modify the grid and add obstacles + grid = [list(line) for line in input.splitlines()] + + # for every coord on the guard's path, just make it an obstacle and see if it loops + ans: int = 0 + for coord in path: + if grid[coord[0]][coord[1]] == ".": + grid[coord[0]][coord[1]] = "#" + if does_guard_loop(grid): + ans += 1 + grid[coord[0]][coord[1]] = "." + + return ans + + +# slight modification to getting the entire path by tracking the direction +# these two could be combined... +def does_guard_loop(grid: list[list[str]]) -> bool: + row: int = 0 + col: int = 0 + + # find starting row and col + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == "^": + row = r + col = c + break + + # example and my input both have the guard just starting pointing north + dir_index: int = 0 + dirs: list[list[int]] = [ + [-1, 0], + [0, 1], + [1, 0], + [0, -1], + ] + + # keys: row, col, dir_index + seen: set[tuple[int, int, int]] = set() + + # assume that we just have to run off the grid?.. + while row >= 0 and row < len(grid) and col >= 0 and col < len(grid[0]): + key = (row, col, dir_index) + + if key in seen: + return True + + seen.add(key) + dir = dirs[dir_index] + nextRow: int = row + dir[0] + nextCol: int = col + dir[1] + if not ( + nextRow >= 0 + and nextRow < len(grid) + and nextCol >= 0 + and nextCol < len(grid[0]) + ): + break + if grid[nextRow][nextCol] == "#": + # rotate + dir_index += 1 + dir_index %= 4 + else: + row = nextRow + col = nextCol + + # no loop found + return False + + +example = """....#..... +.........# +.......... +..#....... +.......#.. +.......... +.#..^..... +........#. +#......... +......#...""" + +input = open("input.txt").read() + + +print(f"part1 example: {part1(example)} want 41") +print(f"part1: {part1(input)} want 4939") + +print(f"part2 example: {part2(example)} want 6") + +start_time = time.time() +print(f"part2: {part2(input)} want 1434") # slow as heck... ~30s on my laptop +end_time = time.time() +print(f"runtime = {end_time - start_time} seconds") diff --git a/2024/day07/day07.py b/2024/day07/day07.py new file mode 100644 index 0000000..1405d88 --- /dev/null +++ b/2024/day07/day07.py @@ -0,0 +1,60 @@ +def day7(input: str, part: int) -> int: + ans: int = 0 + + lines = input.splitlines() + for line in lines: + parts = line.split(": ") + target = int(parts[0]) + nums = [int(x) for x in parts[1].split(" ")] + + if recurse(part, nums, target, 0, 0): + ans += target + + return ans + + +# brute force all the combinations and return True early if the target is made using all elements of nums array +def recurse(part: int, nums: list[int], target: int, current: int, index: int) -> bool: + if target < current: + return False + + if index == len(nums) and current == target: + return True + if index == len(nums) and current != target: + return False + + if index == 0: + current = 1 + multiply_result = recurse(part, nums, target, current * nums[index], index + 1) + if multiply_result: + return True + + # attempt concatenation operation for part 2 + if part == 2: + concat_num = int(str(current) + str(nums[index])) + concat_result = recurse(part, nums, target, concat_num, index + 1) + if concat_result: + return True + + if index == 0: + current = 0 + return recurse(part, nums, target, current + nums[index], index + 1) + + +example = """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""" + +input = open("input.txt").read() + +print(f"part1 example: {day7(example,1)} want 3749") +print(f"part1: {day7(input,1)} want 66343330034722") + +print(f"part2 example: {day7(example,2)} want 11387") +print(f"part2: {day7(input,2)} want 637696070419031") # a bit slow but not bad diff --git a/2024/day08/day08.py b/2024/day08/day08.py new file mode 100644 index 0000000..778e07d --- /dev/null +++ b/2024/day08/day08.py @@ -0,0 +1,120 @@ +from collections import defaultdict + + +def part1(input: str) -> int: + grid = [list(line) for line in input.splitlines()] + map_antenna_to_coords: dict[str, list[tuple[int, int]]] = defaultdict(list) + for i in range(len(grid)): + for j in range(len(grid[0])): + if grid[i][j] != ".": + map_antenna_to_coords[grid[i][j]].append((i, j)) + + rows = len(grid) + cols = len(grid[0]) + + anti_node_coords: set[tuple[int, int]] = set() + + for _, coords in map_antenna_to_coords.items(): + + for i in range(len(coords)): + c1 = coords[i] + for j in range(i + 1, len(coords)): + c2 = coords[j] + + if c1 == c2: + continue + + rowDiff: int = c2[0] - c1[0] + colDiff: int = c2[1] - c1[1] + + maybeRow: int = c1[0] - rowDiff + maybeCol: int = c1[1] - colDiff + if ( + 0 <= maybeRow < rows + and 0 <= maybeCol < cols + and (maybeRow, maybeCol) != c1 + and (maybeRow, maybeCol) != c2 + ): + anti_node_coords.add((maybeRow, maybeCol)) + + maybeRow: int = c2[0] + rowDiff + maybeCol: int = c2[1] + colDiff + if ( + 0 <= maybeRow < rows + and 0 <= maybeCol < cols + and (maybeRow, maybeCol) != c1 + and (maybeRow, maybeCol) != c2 + ): + anti_node_coords.add((maybeRow, maybeCol)) + + return len(anti_node_coords) + + +def part2(input: str) -> int: + grid = [list(line) for line in input.splitlines()] + map_antenna_to_coords: dict[str, list[tuple[int, int]]] = defaultdict(list) + for i in range(len(grid)): + for j in range(len(grid[0])): + if grid[i][j] != ".": + map_antenna_to_coords[grid[i][j]].append((i, j)) + + rows = len(grid) + cols = len(grid[0]) + + anti_node_coords: set[tuple[int, int]] = set() + + for _, coords in map_antenna_to_coords.items(): + + for i in range(len(coords)): + c1 = coords[i] + + # a more elegant solution would be to calculate the starting point of the antenna's line + # then do a single loop to find all anti-node coords + # instead, i'll just go left and right from c1, and add c1 manually + anti_node_coords.add(c1) + + for j in range(i + 1, len(coords)): + c2 = coords[j] + + if c1 == c2: + continue + + rowDiff: int = c2[0] - c1[0] + colDiff: int = c2[1] - c1[1] + + maybeRow: int = c1[0] - rowDiff + maybeCol: int = c1[1] - colDiff + while 0 <= maybeRow < rows and 0 <= maybeCol < cols: + anti_node_coords.add((maybeRow, maybeCol)) + maybeRow -= rowDiff + maybeCol -= colDiff + + maybeRow: int = c1[0] + rowDiff + maybeCol: int = c1[1] + colDiff + while 0 <= maybeRow < rows and 0 <= maybeCol < cols: + anti_node_coords.add((maybeRow, maybeCol)) + maybeRow += rowDiff + maybeCol += colDiff + + return len(anti_node_coords) + + +example = """............ +........0... +.....0...... +.......0.... +....0....... +......A..... +............ +............ +........A... +.........A.. +............ +............""" + +input = open("input.txt").read() + +print(f"part1 example: {part1(example)} want 14") +print(f"part1: {part1(input)} want 396") +print(f"part2 example: {part2(example)} want 34") +print(f"part2: {part2(input)} want 1200") diff --git a/2024/day09/day09.py b/2024/day09/day09.py new file mode 100644 index 0000000..d1272d9 --- /dev/null +++ b/2024/day09/day09.py @@ -0,0 +1,121 @@ +from dataclasses import dataclass + + +# index by index brute force with a sliding window optimization to make it linear... +def part1(input: str) -> int: + total_disk_space: int = 0 + for x in list(input): + total_disk_space += int(x) + + file_system: list[int] = [-1] * total_disk_space + is_file: bool = True + index: int = 0 + file_number: int = 0 + + for x in list(input): + if not is_file: + index += int(x) + is_file = not is_file + else: + for _ in range(int(x)): + if is_file: + file_system[index] = file_number + index += 1 + + file_number += 1 + + is_file = not is_file + + # rearrange file to left via sliding window + left: int = 0 + right: int = len(file_system) - 1 + while left < right: + if file_system[right] == -1: + right -= 1 + elif file_system[left] != -1: + left += 1 + elif file_system[left] == -1: + file_system[left], file_system[right] = ( + file_system[right], + file_system[left], + ) + left += 1 + + # checksum is index in string * number value (file number) + checksum: int = 0 + for i in range(len(file_system)): + if file_system[i] == -1: + break + checksum += i * file_system[i] + + return checksum + + +@dataclass +class FileSystemSpace: + start: int + size: int + file_number: int + + +def part2(input: str) -> int: + files: list[FileSystemSpace] = [] + empty_spaces: list[FileSystemSpace] = [] + + is_file: bool = True + index: int = 0 + for size in [int(x) for x in list(input)]: + file_or_empty = FileSystemSpace(index, size, len(files)) + index += size + if is_file: + files.append(file_or_empty) + else: + empty_spaces.append(file_or_empty) + is_file = not is_file + + # brute force finding a space to move each file into + for file in reversed(files): + for empty_space in empty_spaces: + # prevent moving files to higher spots in the file system... + if empty_space.start > file.start: + break + + # large enough empty space found + if empty_space.size >= file.size: + file.start = empty_space.start + empty_space.start += file.size + empty_space.size -= file.size + + break + + # print_util(files) + + checksum: int = 0 + for file in files: + for x in range(file.size): + checksum += (file.start + x) * file.file_number + + return checksum + + +def print_util(all_files: list[FileSystemSpace]): + last_index: int = 0 + for file in all_files: + last_index = max(last_index, file.start + file.size) + + fs: list[str] = ["."] * (last_index + 1) + for file in all_files: + for i in range(file.start, file.start + file.size): + fs[i] = str(file.file_number) + + print("".join(fs)) + + +example = """2333133121414131402""" +input = open("input.txt").read().strip() + +print(f"part1 example: {part1(example)} want 1928") +print(f"part1: {part1(input)} want 6200294120911") + +print(f"part2 example: {part2(example)} want 2858") +print(f"part2: {part2(input)} want 6227018762750") diff --git a/2024/day10/day10.py b/2024/day10/day10.py new file mode 100644 index 0000000..396631b --- /dev/null +++ b/2024/day10/day10.py @@ -0,0 +1,99 @@ +def part1(input: str) -> int: + grid: list[list[int, int]] = [] + for line in input.splitlines(): + grid.append([int(x) for x in list(line)]) + # alternative pythonic way... + # grid = [[int(char) for char in line] for line in input.splitlines()] + + ans: int = 0 + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == 0: + ans += len(dfs_backtrack_unique_end_coords(grid, r, c, {(r, c)})) + + return ans + + +# I misread the instructions slightly and went for a "unique paths" algo at first. +# It would be simpler to pass the "reachable 9s coords" set in as an arg and update +# it in the termination case, then take the length of that set in the part1() function. +# This would remove the need to combine the sets which is potentially expensive (and ugly) +def dfs_backtrack_unique_end_coords( + grid: list[list[int, int]], row: int, col: int, visited: set[tuple[int, int]] +) -> set[tuple[int, int]]: + if grid[row][col] == 9: + return {(row, col)} + + all_coords: set[tuple[int, int]] = set() + for diff in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + nextRow = row + diff[0] + nextCol = col + diff[1] + + if not (0 <= nextRow < len(grid) and 0 <= nextCol < len(grid[0])): + continue + if (nextRow, nextCol) in visited: + continue + if grid[nextRow][nextCol] == grid[row][col] + 1: + visited.add((nextRow, nextCol)) + new_coords = dfs_backtrack_unique_end_coords( + grid, nextRow, nextCol, visited + ) + # combines the two sets, see comment above function def + all_coords.update(new_coords) + visited.remove((nextRow, nextCol)) + + return all_coords + + +def part2(input: str) -> int: + grid = [[int(char) for char in line] for line in input.splitlines()] + + ans: int = 0 + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == 0: + ans += dfs_backtrack_unique_paths(grid, r, c, {(r, c)}) + + return ans + + +def dfs_backtrack_unique_paths( + grid: list[list[int, int]], row: int, col: int, visited: set[tuple[int, int]] +) -> int: + if grid[row][col] == 9: + # unique path found + return 1 + + total: int = 0 + for diff in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + nextRow = row + diff[0] + nextCol = col + diff[1] + + if not (0 <= nextRow < len(grid) and 0 <= nextCol < len(grid[0])): + continue + if (nextRow, nextCol) in visited: + continue + if grid[nextRow][nextCol] == grid[row][col] + 1: + visited.add((nextRow, nextCol)) + total += dfs_backtrack_unique_paths(grid, nextRow, nextCol, visited) + visited.remove((nextRow, nextCol)) + + return total + + +example = """89010123 +78121874 +87430965 +96549874 +45678903 +32019012 +01329801 +10456732""" + +input = open("input.txt").read().strip() + +print(f"part1 example: {part1(example)} want 36") +print(f"part1: {part1(input)} want 782") + +print(f"part2 example: {part2(example)} want 81") +print(f"part2 example: {part2(input)} want 1694") diff --git a/2024/day11/day11.py b/2024/day11/day11.py new file mode 100644 index 0000000..db3a51d --- /dev/null +++ b/2024/day11/day11.py @@ -0,0 +1,50 @@ +# If the stone is engraved with the number 0, it is replaced by a stone engraved with the number 1. +# If the stone is engraved with a number that has an even number of digits, it is replaced by two stones. The left half of the digits are engraved on the new left stone, and the right half of the digits are engraved on the new right stone. (The new numbers don't keep extra leading zeroes: 1000 would become stones 10 and 0.) +# If none of the other rules apply, the stone is replaced by a new stone; the old stone's number multiplied by 2024 is engraved on the new stone. + + +# actually modelling this would be a pain in the ass, error-prone, and probably +# not fast enough for part 2 where there will presumably be more blinks +# instead if we only care about the final number of stones, we can just see +# how many stone each original stone splits into, and we can memoize it +def day11(input: str, blinks: int) -> int: + total_stones: int = 0 + memo: dict[tuple[int, int], int] = {} + for stone in input.split(" "): + total_stones += calculate_final_stones_count(stone, blinks, memo) + return total_stones + + +def calculate_final_stones_count(num_as_str: str, blinks_left: int, memo) -> int: + key = (num_as_str, blinks_left) + if key in memo: + return memo[key] + + if blinks_left == 0: + return 1 + + total_stones: int = 0 + if num_as_str == "0": + total_stones += calculate_final_stones_count("1", blinks_left - 1, memo) + elif len(num_as_str) % 2 == 0: + # convert back and forth again to get rid of leading zeroes + left_num = str(int(num_as_str[: len(num_as_str) // 2])) + right_num = str(int(num_as_str[len(num_as_str) // 2 :])) + total_stones += calculate_final_stones_count(left_num, blinks_left - 1, memo) + total_stones += calculate_final_stones_count(right_num, blinks_left - 1, memo) + else: + new_num: int = str(int(num_as_str) * 2024) + total_stones += calculate_final_stones_count(new_num, blinks_left - 1, memo) + + memo[key] = total_stones + + return total_stones + + +example = """125 17""" +input = open("input.txt").read().strip() + +print(f"part1 example: {day11(example, 6)} want 22") +print(f"part1 example: {day11(example, 25)} want 55312") +print(f"part1: {day11(input, 25)} want 189092") +print(f"part2: {day11(input, 75)} want 224869647102559") diff --git a/2024/day12/day12.py b/2024/day12/day12.py new file mode 100644 index 0000000..553104a --- /dev/null +++ b/2024/day12/day12.py @@ -0,0 +1,164 @@ +from collections import defaultdict + + +def day12(input: str, part: int) -> int: + + grid = [list(line) for line in input.splitlines()] + visited: set[tuple[int, int]] = {} + cost: int = 0 + + for r in range(len(grid)): + for c in range(len(grid[0])): + coord = (r, c) + if coord not in visited: + island_coords: set[tuple[int, int]] = set() + + flood_fill_island(grid, r, c, visited, island_coords) + if part == 1: + cost += len(island_coords) * get_perimeter_of_island(island_coords) + elif part == 2: + edge_count: int = get_edge_count_of_island(island_coords) + cost += len(island_coords) * edge_count + else: + raise ("unexpected part") + + return cost + + +# refactored to just populate the entire island_coords set and visited set +# just populates the island_coords so does not need to return anything +def flood_fill_island( + grid: list[list[str]], + row: int, + col: int, + visited: set[tuple[int, int]], + island_coords: set[tuple[int, int]], +): + visited[(row, col)] = True + island_coords.add((row, col)) + + for diff in [(0, -1), (0, 1), (-1, 0), (1, 0)]: + next_row = row + diff[0] + next_col = col + diff[1] + + if 0 <= next_row < len(grid) and 0 <= next_col < len(grid[0]): + if (next_row, next_col) in visited: + continue + # if in range, check if neighbor is same to recurse + if grid[next_row][next_col] == grid[row][col]: + # if does match and unvisited, recurse + flood_fill_island(grid, next_row, next_col, visited, island_coords) + + +# for part1 cost calculation +def get_perimeter_of_island(island_coords: set[tuple[int, int]]) -> int: + perimeter: int = 0 + for coord in island_coords: + for diff in [(0, -1), (0, 1), (-1, 0), (1, 0)]: + next_coord = (coord[0] + diff[0], coord[1] + diff[1]) + if next_coord not in island_coords: + perimeter += 1 + + return perimeter + + +def get_edge_count_of_island(island_coords: set[tuple[int, int]]) -> int: + edges: int = 0 + + map_dir_to_coord: dict[tuple[int, int], set[tuple[int, int]]] = defaultdict(set) + + for coord in island_coords: + for diff in [(0, -1), (0, 1), (-1, 0), (1, 0)]: + # this coord has already been accounted for in an edge + if coord in map_dir_to_coord[diff]: + continue + + next_coord = (coord[0] + diff[0], coord[1] + diff[1]) + # not in the island means it is bordering an edge... + if next_coord not in island_coords: + # collect all coords that make up this same edge, basically we need to go perpendicular to diff + collect_all_coords_on_edge(island_coords, coord, diff, map_dir_to_coord) + edges += 1 + + return edges + + +perpendicular_dirs: dict[tuple[int, int], list[tuple[int, int]]] = { + (0, -1): [(-1, 0), (1, 0)], + (0, 1): [(-1, 0), (1, 0)], + (-1, 0): [(0, -1), (0, 1)], + (1, 0): [(0, -1), (0, 1)], +} + + +def collect_all_coords_on_edge( + island_coords: set[tuple[int, int]], + coord: tuple[int, int], + empty_dir_diff: tuple[int, int], + map_dir_to_coord: dict[tuple[int, int], set[tuple[int, int]]], +): + # mark self + map_dir_to_coord[empty_dir_diff].add(coord) + + for perp in perpendicular_dirs[empty_dir_diff]: + next_coord = (coord[0] + perp[0], coord[1] + perp[1]) + # we're collecting the entire connected edge that is facing a single direction, + # so stop checking if we're "off" the island + # do not need to check if we're inside the grid because we can just leverage island_coords + if next_coord not in island_coords: + continue + # if already visited for facing this direction, we can also skip + if next_coord in map_dir_to_coord[empty_dir_diff]: + continue + + # continue if next_coord is not a contiguous part of the edge + if (next_coord[0] + empty_dir_diff[0], next_coord[1] + empty_dir_diff[1]) in island_coords: + continue + + # need to continue exploring recursively + collect_all_coords_on_edge( + island_coords, next_coord, empty_dir_diff, map_dir_to_coord + ) + + +small_example = """AAAA +BBCD +BBCC +EEEC""" + + +example = """RRRRIICCFF +RRRRIICCCF +VVRRRCCFFF +VVRCCCJFFF +VVVVCJJCFE +VVIVCCJJEE +VVIIICJJEE +MIIIIIJJEE +MIIISIJEEE +MMMISSJEEE""" + +part2_example = """AAAAAA +AAABBA +AAABBA +ABBAAA +ABBAAA +AAAAAA""" + +input = open("input.txt").read().strip() + +print(f"part1 small_example: {day12(small_example, 1)} want 140") +print(f"part1 example: {day12(example, 1)} want 1930") +print(f"part1: {day12(input, 1)} want 1473408") + +print(f"part2 small_example: {day12(small_example, 2)} want 80") +# this one helped debug the disjointed edges bug (second and fourth rows pointing east) +print(f"part2 example: {day12("""EEEEE +EXXXX +EEEEE +EXXXX +EEEEE""", 2)} want 236") +print(f"part2 part2_example: {day12(part2_example, 2)} want 368") +print(f"part2 example: {day12(example, 2)} want 1206") +print(f"part2: {day12(input, 2)} want 886364") + diff --git a/2024/day13/day13.py b/2024/day13/day13.py new file mode 100644 index 0000000..52d70fd --- /dev/null +++ b/2024/day13/day13.py @@ -0,0 +1,62 @@ +import re + + +def part1(input: str, part: int) -> int: + ans: int = 0 + + for machine in input.split("\n\n"): + lines = machine.split("\n") + a_parts: list[str] = re.findall(r"\d+", lines[0]) + Ax, Ay = int(a_parts[0]), int(a_parts[1]) + + b_parts: list[str] = re.findall(r"\d+", lines[1]) + Bx, By = int(b_parts[0]), int(b_parts[1]) + + prize_parts: list[str] = re.findall(r"\d+", lines[2]) + Px, Py = int(prize_parts[0]), int(prize_parts[1]) + + if part == 2: + Px += 10000000000000 + Py += 10000000000000 + + # Ax * a + Bx * b = Px + # Ay * a + By * b = Py + # Solve for a and b... + # a = (Px - Bx * b) / Ax + # b * (By * Ax - Ay * Bx) = Py * Ax - Ay * Px + b = (Py * Ax - Ay * Px) / (By * Ax - Ay * Bx) + a = (Px - Bx * b) / Ax + + if b % 1 == 0 and a % 1 == 0: + ans += 3 * a + b + + return int(ans) + + +# A 3, B 1 +# moves right along X, forward along Y + +example = """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""" + +input = open("input.txt").read().strip() + +print(f"part1 example: {part1(example, 1)} want 480") +print(f"part1: {part1(input, 1)} want 28059") + +# don't think this result was given in the prompt +print(f"part1 example: {part1(example, 2)} want 875318608908") +print(f"part1: {part1(input, 2)} want 102255878088512") diff --git a/2024/day14/day14.py b/2024/day14/day14.py new file mode 100644 index 0000000..1825150 --- /dev/null +++ b/2024/day14/day14.py @@ -0,0 +1,108 @@ +import re + + +def part1(input: str, wide: int, tall: int, seconds: int) -> int: + # don't need to model this exactly right? just equate a line, mod by grid size... + # i guess for 100 steps it would've been better to just model it... + + quad_counts: list[int] = [0] * 4 + + for line in input.splitlines(): + nums = re.findall(r"-?\d+", line) + assert len(nums) == 4 + + x = int(nums[0]) + int(nums[2]) * seconds + y = int(nums[1]) + int(nums[3]) * seconds + + x %= tall + y %= wide + + if x < tall // 2 and y < wide // 2: + # print("top left") + quad_counts[0] += 1 + elif x < tall // 2 and y > wide // 2: + # print("top right") + quad_counts[1] += 1 + elif x > tall // 2 and y < wide // 2: + # print("bottom left") + quad_counts[2] += 1 + elif x > tall // 2 and y > wide // 2: + # print("bottom right") + quad_counts[3] += 1 + # else: + # print("on mid line") + + # multiply robot count in each quadrant + # does not include robots on mid-lines + return quad_counts[0] * quad_counts[1] * quad_counts[2] * quad_counts[3] + + +def part2(input: str, wide: int, tall: int) -> int: + # i guess for 100 steps it would've been better to just model it... + # then i could have reused it for part 2... + + robots: list[list[int]] = [] + for line in input.splitlines(): + nums = re.findall(r"-?\d+", line) + assert len(nums) == 4 + robots.append([int(x) for x in nums]) + + for s in range(10_000): + + neighbors: set[tuple[int, int]] = set() + matched: set[tuple[int, int]] = set() + for robot in robots: + robot[0] += robot[2] + robot[1] += robot[3] + + robot[0] %= tall + robot[1] %= wide + + # track how many neighboring robots have been placed so far + # when the number is high enough we probably have some image of a tree + coord = (robot[0], robot[1]) + if coord in neighbors: + matched.add(coord) + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + neighbors.add((coord[0] + dx, coord[1] + dy)) + + if len(matched) >= 250: + print_grid(robots, wide, tall) + # print(len(matched)) + return s + 1 + + raise Exception("should return from loop") + + +def print_grid(robots: list[list[int]], wide: int, tall: int): + grid: list[list[str]] = [] + for _ in range(0, tall): + grid.append([" "] * wide) + + for robot in robots: + grid[robot[0]][robot[1]] = "X" + + for line in grid: + print("".join(line)) + + +example = """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""" + +input = open("input.txt").read().strip() + +print(f"part1 example: {part1(example, 7, 11, 100)} want 12") +print(f"part1 example: {part1(input, 103, 101, 100)} want 218965032") + +print(f"part2: {part2(input, 103, 101)} want 7037") diff --git a/2024/day15/day15.py b/2024/day15/day15.py new file mode 100644 index 0000000..97c11dd --- /dev/null +++ b/2024/day15/day15.py @@ -0,0 +1,214 @@ +diff_map: dict[str, tuple[int, int]] = { + "^": (-1, 0), + "v": (1, 0), + "<": (0, -1), + ">": (0, 1), +} + + +def part1(input: str) -> int: + input_parts = input.split("\n\n") + grid = [list(line) for line in input_parts[0].splitlines()] + + row: int = 0 + col: int = 0 + + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == "@": + row = r + col = c + + instructions = "".join(input_parts[1].splitlines()) + for inst in instructions: + diff = diff_map[inst] + next_row, next_col = row + diff[0], col + diff[1] + + match grid[next_row][next_col]: + case "#": + # blocked + continue + case ".": + grid[row][col] = "." + grid[next_row][next_col] = "@" + row, col = next_row, next_col + case "O": + # attempt push, keep moving in direction of diff until a . or # is hit... + not_obstacle_row, not_obstacle_col = next_row, next_col + while grid[not_obstacle_row][not_obstacle_col] == "O": + not_obstacle_row += diff[0] + not_obstacle_col += diff[1] + + # if it's a wall "#", nothing moves, so only check for empty spaces "." + if grid[not_obstacle_row][not_obstacle_col] == ".": + grid[not_obstacle_row][not_obstacle_col] = "O" + grid[next_row][next_col] = "@" + grid[row][col] = "." + row, col = next_row, next_col + case _: + raise Exception("unhandled grid type: ", grid[next_row][next_col]) + + # print("\n".join(["".join(row) for row in grid])) + + # 100 times its distance from the top edge of the map plus its distance from the left edge of the map + # 0-indexed so same as indices in 2D array + gps_sum: int = 0 + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == "O": + gps_sum += r * 100 + c + + return gps_sum + + +def part2(input: str) -> int: + input_parts = input.split("\n\n") + + input_parts[0] = input_parts[0].replace("O", "[]") + input_parts[0] = input_parts[0].replace(".", "..") + input_parts[0] = input_parts[0].replace("#", "##") + input_parts[0] = input_parts[0].replace("@", "@.") + + grid = [list(line) for line in input_parts[0].splitlines()] + + row: int = 0 + col: int = 0 + + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == "@": + row = r + col = c + + # print("\n".join(["".join(row) for row in grid])) + + instructions = "".join(input_parts[1].splitlines()) + for inst in instructions: + # first see what's in the next space we want to move into... + diff = diff_map[inst] + next_row, next_col = row + diff[0], col + diff[1] + match grid[next_row][next_col]: + case "#": + continue + case ".": + grid[row][col] = "." + row += diff[0] + col += diff[1] + grid[row][col] = "@" + case "[" | "]": + # handle left and right separately because they only push one row of boxes + # at this point row == next_row + if inst in "<>": + # copy-pasta from part 1 + not_obstacle_col = next_col + while grid[next_row][not_obstacle_col] in "[]": + not_obstacle_col += diff[1] + + # if it's a wall "#", nothing moves, so only check for empty spaces "." + # move everything over towards the diff direction + if grid[next_row][not_obstacle_col] == ".": + for c in range(not_obstacle_col, col, -1 * diff[1]): + grid[row][c] = grid[row][c - diff[1]] + + # move robot.. + grid[next_row][next_col] = "@" + grid[row][col] = "." + row, col = next_row, next_col + else: + # push boxes up or down, use a stack to maintain the reverse order of boxes that + # MAY get moved + coords_to_check: list[tuple[int, int]] = [ + (row, col), + ] + + to_check_index: int = 0 + is_blocked: bool = False + while to_check_index < len(coords_to_check): + front = coords_to_check[to_check_index] + to_check_index += 1 + + # variable masking is no bueno, but i guess it's fine here + next_row, next_col = front[0] + diff[0], front[1] + diff[1] + next_value = grid[next_row][next_col] + if next_value == "#": + is_blocked = True + break + if next_value == "[": + coords_to_check.append((next_row, next_col)) + coords_to_check.append((next_row, next_col + 1)) + if next_value == "]": + coords_to_check.append((next_row, next_col)) + coords_to_check.append((next_row, next_col - 1)) + # if "." then do nothing... no need to check + + # move everything towards diff if nothing was blocked + # in reverse order to avoid overwrites + if not is_blocked: + # didn't prevent duplicate adds in the "checking" stage, so just no-op them here + moved_set: set[tuple[int, int]] = set() + for coord in reversed(coords_to_check): + if coord in moved_set: + continue + moved_set.add(coord) + + next_row, next_col = coord[0] + diff[0], coord[1] + diff[1] + grid[next_row][next_col], grid[coord[0]][coord[1]] = ( + grid[coord[0]][coord[1]], + grid[next_row][next_col], + ) + row, col = next_row, next_col + + case _: + raise Exception("unexpected grid value: ", grid[next_row][next_col]) + # print("\n".join(["".join(row) for row in grid])) + + gps_sum: int = 0 + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == "[": + gps_sum += r * 100 + c + + return gps_sum + + +small_example = """######## +#..O.O.# +##@.O..# +#...O..# +#.#.O..# +#...O..# +#......# +######## + +<^^>>>vv>v<<""" + +example = """########## +#..O..O.O# +#......O.# +#.OO..O.O# +#..O@..O.# +#O#..O...# +#O..O..O.# +#.OO.O.OO# +#....O...# +########## + +^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^ +vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv< +<>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^ +^><^><>>><>^^<<^^v>>><^^>v>>>^v><>^v><<<>vvvv>^<><<>^>< +^>><>^v<><^vvv<^^<><^v<<<><<<^^<^>>^<<<^>>^v^>>^v>vv>^<<^v<>><<><<>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^ +<><^^>^^^<>^vv<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<> +^^>vv<^v^v^<>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<>< +v^^>>><<^^<>>^v^v^<<>^<^v^v><^<<<><<^vv>>v>v^<<^""" + +input = open("input.txt").read().strip() + +print(f"part1 small_example: {part1(small_example)} want 2028") +print(f"part1 example: {part1(example)} want 10092") +print(f"part1: {part1(input)} want 1413675") + +print(f"part2 example: {part2(example)} want 9021") +print(f"part2: {part2(input)} want 1399772") diff --git a/2024/day16/day16.py b/2024/day16/day16.py new file mode 100644 index 0000000..cac73bd --- /dev/null +++ b/2024/day16/day16.py @@ -0,0 +1,135 @@ +import heapq + + +diffs: list[tuple[int, int]] = [ + (0, 1), # start at index 0 facing "east"/right + (-1, 0), # up + (0, -1), # left + (1, 0), # down +] + + +def day16(input: str, part: int) -> int: + grid = [list(line) for line in input.splitlines()] + start_row: int = 0 + start_col: int = 0 + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == "S": + start_row = r + start_col = c + + # i think i'd rather write a struct in go... these tuples are easy to make + # but annoying to maintain in my head... + # score, row, col, dir_index + node: tuple[int, int, int, int, set[tuple[int, int]]] = ( + int(0), + start_row, + start_col, + int(0), + set(), + ) + min_heap = [node] + heapq.heapify(min_heap) + + coord_to_min_score: dict[tuple[int, int, int], int] = {} + + best_score: int = 1_000_000 # big enough to not interfere with actual input answer + final_path_coords: set[tuple[int, int]] = set() + + while len(min_heap) > 0: + popped = heapq.heappop(min_heap) + + # part2 exit once best_score is set down to the actual min value + if popped[0] > best_score: + continue + + # row, col, dir_index + coord = (popped[1], popped[2], popped[3]) + if coord in coord_to_min_score: + prev_score = coord_to_min_score[coord] + # if previous score at this coord and direction is better then continue + if popped[0] > prev_score: + continue + + # update best score to reach this coord + coord_to_min_score[coord] = popped[0] + + # end reached + if grid[popped[1]][popped[2]] == "E": + best_score = popped[0] + final_path_coords |= popped[4] + final_path_coords.add((popped[1], popped[2])) + continue + + # in same direction + diff = diffs[popped[3]] + next = ( + popped[0] + 1, + popped[1] + diff[0], + popped[2] + diff[1], + popped[3], + popped[4] | {(popped[1], popped[2])}, + ) + if grid[next[1]][next[2]] in ".E": + heapq.heappush(min_heap, next) + + # 90 deg turns + heapq.heappush( + min_heap, + (popped[0] + 1000, popped[1], popped[2], (popped[3] + 1) % 4, popped[4]), + ) + heapq.heappush( + min_heap, + (popped[0] + 1000, popped[1], popped[2], (popped[3] - 1) % 4, popped[4]), + ) + + if part == 1: + return best_score + + return len(final_path_coords) + + +example = """############### +#.......#....E# +#.#.###.#.###.# +#.....#.#...#.# +#.###.#####.#.# +#.#.#.......#.# +#.#.#####.###.# +#...........#.# +###.#.#####.#.# +#...#.....#.#.# +#.#.#.###.#.#.# +#.....#...#.#.# +#.###.#.#.#.#.# +#S..#.....#...# +###############""" + +example2 = """################# +#...#...#...#..E# +#.#.#.#.#.#.#.#.# +#.#.#.#...#...#.# +#.#.#.#.###.#.#.# +#...#.#.#.....#.# +#.#.#.#.#.#####.# +#.#...#.#.#.....# +#.#.#####.#.###.# +#.#.#.......#...# +#.#.###.#####.### +#.#.#...#.....#.# +#.#.#.#####.###.# +#.#.#.........#.# +#.#.#.#########.# +#S#.............# +#################""" + +input = open("input.txt").read().strip() + +print(f"day16 example: {day16(example, 1)} want 7036") +print(f"day16 example2: {day16(example2,1)} want 11048") +print(f"day16 actual: {day16(input,1)} want 83444") + +print(f"part2 example: {day16(example,2)} want 45") +print(f"part2 example2: {day16(example2,2)} want 64") +print(f"part2 actual: {day16(input,2)} want 483") diff --git a/2024/day17/day17.py b/2024/day17/day17.py new file mode 100644 index 0000000..6f502e5 --- /dev/null +++ b/2024/day17/day17.py @@ -0,0 +1,131 @@ +import re + + +def part1(input: str) -> str: + matches = re.findall(r"\d+", input) + + reg_a: int = int(matches[0]) + reg_b: int = int(matches[1]) + reg_c: int = int(matches[2]) + + program: list[int] = [int(x) for x in matches[3:]] + + out = run(program, reg_a, reg_b, reg_c) + return ",".join([str(v) for v in out]) + + +def run(program: list[int], reg_a: int, reg_b: int, reg_c: int) -> list[int]: + def get_combo_operand(combo: int) -> int: + if 0 <= combo <= 3: + return combo + if combo == 4: + return reg_a + if combo == 5: + return reg_b + if combo == 6: + return reg_c + raise Exception("unexpected combo value: ", combo) + + i: int = 0 + out: list[int] = [] + while i < len(program): + opcode = program[i] + operand = program[i + 1] + match opcode: + case 0: + reg_a = reg_a // (2 ** get_combo_operand(operand)) + i += 2 + case 1: + reg_b = reg_b ^ operand + i += 2 + case 2: + reg_b = get_combo_operand(operand) % 8 + i += 2 + case 3: + if reg_a != 0: + i = operand + else: + i += 2 + case 4: + reg_b = reg_b ^ reg_c + i += 2 + case 5: + out.append(get_combo_operand(operand) % 8) + i += 2 + case 6: + reg_b = reg_a // (2 ** get_combo_operand(operand)) + i += 2 + case 7: + reg_c = reg_a // (2 ** get_combo_operand(operand)) + i += 2 + case _: + raise Exception("unhandled opcode", opcode) + + return out + + +# only works on actual input +def part2() -> int: + matches = re.findall(r"\d+", input) + program: list[int] = [int(x) for x in matches[3:]] + + # generate each element of the program one at a time, starting at the end + output = str + reg_as: list[int] = [] + for i in range(1, 8): + output = run_optimized(i) + if output == program[-len(output) :]: + reg_as.append(i) + + digit_count = 1 + while digit_count < 16: + next_reg_As: list[int] = [] + for a in reg_as: + a *= 8 + for i in range(8): + output = run_optimized(a + i) + if output == program[-len(output) :]: + next_reg_As.append(a + i) + reg_as = next_reg_As + digit_count += 1 + + # first reg_as will be smallest + return reg_as[0] + + +def run_optimized(a: int) -> list[int]: + b: int = 0 + output: list[int] = [] + while a != 0: + # 2,4, 1,1, 7,5, 4,0, 0,3, 1,6, 5,5, 3,0 + # pen and paper "algebra" + b = ((a % 8) ^ 1) ^ (a // (2 ** ((a % 8) ^ 1))) ^ 6 + output.append(b % 8) + a = a // 8 + return output + + +example = """Register A: 729 +Register B: 0 +Register C: 0 + +Program: 0,1,5,4,3,0""" + +input = open("input.txt").read().strip() + +print(f"part1 example: {part1(example)} want '4,6,3,5,6,3,5,2,1,0'") +print(f"part1 actual: {part1(input)} want '1,6,3,6,5,6,5,1,7'") + +print( + f"optimized {",".join([str(x) for x in run_optimized(30899381)])} want {part1(input)}" +) + + +example_part2 = """Register A: 2024 +Register B: 0 +Register C: 0 + +Program: 0,3,5,4,3,0""" + +# print(f"part2 example: {part1(example_part2, 2)} want 117440") +print(f"part2 actual: {part2()} want 247839653009594") diff --git a/2024/day18/day18.py b/2024/day18/day18.py new file mode 100644 index 0000000..c17e1ef --- /dev/null +++ b/2024/day18/day18.py @@ -0,0 +1,112 @@ +def part1(input: str, grid_size: int, fallen_bytes: int) -> int: + # simulate fallen_bytes + # path find from top left to bottom right + + grid = [["." for _ in range(grid_size)] for _ in range(grid_size)] + + for line in input.splitlines()[:fallen_bytes]: + parts = line.split(",") + col = int(parts[0]) + row = int(parts[1]) + grid[row][col] = "#" + + queue: list[tuple[int, int, int]] = [(0, 0, 0)] + visited: set[tuple[int, int]] = set() + while len(queue) > 0: + front = queue.pop(0) + + if front[:2] in visited: + continue + visited.add(front[:2]) + + if front[0] == grid_size - 1 and front[1] == grid_size - 1: + return front[2] + for diff in [(0, 1), (1, 0), (0, -1), (-1, 0)]: + next_row = front[0] + diff[0] + next_col = front[1] + diff[1] + if 0 <= next_row < grid_size and 0 <= next_col < grid_size: + if grid[next_row][next_col] != "#": + queue.append((next_row, next_col, front[2] + 1)) + + raise Exception("should return from loop") + + +def part2(input: str, grid_size: int) -> str: + grid = [["." for _ in range(grid_size)] for _ in range(grid_size)] + + # drop all fallen bytes, then remove them selectively and attempt to continue + # path finding from there? + fallen_bytes: list[tuple[int, int]] = [] + for line in input.splitlines(): + parts = line.split(",") + col = int(parts[0]) + row = int(parts[1]) + grid[row][col] = "#" + fallen_bytes.append((row, col)) + + # get set of all reachable coords from the starting point + reachable: set[tuple[int, int]] = {(0, 0)} + flood_fill_from(grid, (0, 0), reachable) + + # iterate in reverse through fallen bytes and if the byte is adjacent to a reachable + # cell, then try to flood fill from the adjacent cell's neighbors + for fallen in reversed(fallen_bytes): + grid[fallen[0]][fallen[1]] = "." + for diff in [(0, 1), (1, 0), (0, -1), (-1, 0)]: + next = (fallen[0] + diff[0], fallen[1] + diff[1]) + if next in reachable: + if flood_fill_from(grid, next, reachable): + return f"{fallen[1]},{fallen[0]}" + + raise Exception("should return from loop") + + +def flood_fill_from( + grid: list[list[str]], coord: tuple[int, int], reachable: set[tuple[int, int]] +) -> bool: + for diff in [(0, 1), (1, 0), (0, -1), (-1, 0)]: + next_row = coord[0] + diff[0] + next_col = coord[1] + diff[1] + if (next_row, next_col) not in reachable: + if 0 <= next_row < len(grid) and 0 <= next_col < len(grid): + if grid[next_row][next_col] != "#": + reachable.add((next_row, next_col)) + flood_fill_from(grid, (next_row, next_col), reachable) + + return (len(grid) - 1, len(grid) - 1) in reachable + + +# col, row (x, y) +example = """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""" + +input = open("input.txt").read().strip() + +print(f"part1 example: {part1(example, 6 + 1, 12)} want 22") +print(f"part1 actual: {part1(input, 70 + 1, 1024)} want 298") + +print(f"part2 example: {part2(example, 6 + 1)} want '6,1'") +print(f"part2 actual: {part2(input, 70 + 1)} want 52,32") diff --git a/2024/day19/day19.py b/2024/day19/day19.py new file mode 100644 index 0000000..2caebc0 --- /dev/null +++ b/2024/day19/day19.py @@ -0,0 +1,87 @@ +from dataclasses import dataclass +from typing import Self + + +@dataclass +class TrieNode: + char: str + is_terminator: bool + children: dict[str, Self] + + +def day19(input: str, part: int) -> int: + # trie data structure works nicely... but is it necessary for part 2... + input_lines = input.splitlines() + + root = TrieNode("", False, {}) + for towel in input_lines[0].split(", "): + iter = root + for i, char in enumerate(towel): + if char not in iter.children: + iter.children[char] = TrieNode(char, i == len(towel) - 1, {}) + iter = iter.children[char] + # update for matching towels that are smaller than a pervious one + # e.g. "towel" then "tow" needs to update the "w" node + iter.is_terminator |= i == len(towel) - 1 + + possible_towels: int = 0 + total: int = 0 + + memo: dict[str, int] = {} + for maybe_towel in input_lines[2:]: + combos = is_possible_towel(root, maybe_towel, memo) + if combos > 0: + possible_towels += 1 + total += combos + + if part == 1: + return possible_towels + + return total + + +# memo optimization added for part 2 +def is_possible_towel(trie: TrieNode, towel: str, memo: dict[str, int]) -> int: + if towel == "": + return 1 + if towel in memo: + return memo[towel] + + iter = trie + total: int = 0 + for i, char in enumerate(towel): + if char not in iter.children: + break + + iter = iter.children[char] + + # if iterated to a terminator, we can restart recursively and if that + # "works" (aka returns positive number of possible combos), then we can + # sum the combos and then continue along this for loop to account for + # larger towels that do not end at this index + if iter.is_terminator: + total += is_possible_towel(trie, towel[i + 1 :], memo) + + memo[towel] = total + + return total + + +example = """r, wr, b, g, bwu, rb, gb, br + +brwrr +bggr +gbbr +rrbgbr +ubwu +bwurrg +brgr +bbrgwb""" + +input = open("input.txt").read().strip() + +print(f"day19 example: {day19(example, 1)}, want 6") +print(f"day19 actual: {day19(input, 1)}, want 216") + +print(f"part2 example: {day19(example ,2)}, want 16") +print(f"part2 actual: {day19(input ,2)}, want 603191454138773") diff --git a/2024/day20/day20.py b/2024/day20/day20.py new file mode 100644 index 0000000..b625c76 --- /dev/null +++ b/2024/day20/day20.py @@ -0,0 +1,170 @@ +from collections import defaultdict + + +def day20(input: str, cheats: int) -> dict[int, int]: + # get minimum without cheats, do this backwards by propagating from E to S + # to populate a grid of the distance from each cell to E + grid: list[list[str]] = [list(line) for line in input.splitlines()] + + row: int = 0 + col: int = 0 + + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == "E": + row = r + col = c + + # default value set to 100_000 which is bigger than entire grid's area so + # walls will not be usable as paths to finish + dp_steps_to_end: list[list[int]] = [ + [100_000 for _ in range(len(grid[0]))] for _ in range(len(grid)) + ] + + diffs = [(0, 1), (0, -1), (-1, 0), (1, 0)] + queue: list[tuple[int, int, int]] = [(row, col, 0)] + visited: set[tuple[int, int]] = set() + while len(queue) > 0: + row, col, steps = queue.pop(0) + dp_steps_to_end[row][col] = steps + visited.add((row, col)) + + for diff in diffs: + next_row, next_col = row + diff[0], col + diff[1] + if 0 <= next_row < len(grid) and 0 <= next_col < len(grid[0]): + if grid[next_row][next_col] != "#": + if (next_row, next_col) not in visited: + queue.append((next_row, next_col, steps + 1)) + + # then starting from S, BFS but from each cell, view all cells cells away + # if it's a valid shortcut, record it + + second_saved_freqs: dict[int, int] = defaultdict(int) + + reachable_diffs = get_reachable_coords_within_x_steps(cheats) + + # visited contains coords of the entire path, so just use that... + for path_coord in visited: + r, c = path_coord + for diff in reachable_diffs: + rr = path_coord[0] + diff[0] + cc = path_coord[1] + diff[1] + + if 0 <= rr < len(grid) and 0 <= cc < len(grid[0]): + if grid[rr][cc] != "#": + savings = dp_steps_to_end[r][c] - ( + dp_steps_to_end[rr][cc] + abs(diff[0]) + abs(diff[1]) + ) + if savings > 0: + second_saved_freqs[savings] += 1 + + return second_saved_freqs + + +# helper function to get all coords that are reachable within allotted cheats +def get_reachable_coords_within_x_steps(x: int) -> list[tuple[int, int]]: + coords_map: dict[tuple[int, int], bool] = {} + + queue: list[tuple[int, int]] = [(0, 0)] + for _ in range(x): + next_queue: list[tuple[int, int]] = [] + while len(queue) > 0: + front = queue.pop(0) + for diff in [(0, -1), (0, 1), (-1, 0), (1, 0)]: + coord = (front[0] + diff[0], front[1] + diff[1]) + if coord not in coords_map: + coords_map[coord] = True + next_queue.append(coord) + queue = next_queue + + return list(coords_map.keys()) + + +example = """############### +#...#...#.....# +#.#.#.#.#.###.# +#S#...#.#.#...# +#######.#.#.### +#######.#.#...# +#######.#.###.# +###..E#...#...# +###.#######.### +#...###...#...# +#.#####.#.###.# +#.#...#.#.#...# +#.#.#.#.#.#.### +#...#...#...### +###############""" + +# python is weird and we can use == to compare 2 dicts for the same keys and values... +example_seconds_saved_to_freq: dict[int, int] = { + 2: 14, + 4: 14, + 6: 2, + 8: 4, + 10: 2, + 12: 3, + 20: 1, + 36: 1, + 38: 1, + 40: 1, + 64: 1, +} + +part1_example_result = day20(example, 2) +print( + f"part1 example: {part1_example_result == example_seconds_saved_to_freq} want True" +) +if part1_example_result != example_seconds_saved_to_freq: + print( + f"\tDEBUG part1 example got: {part1_example_result} want {(example_seconds_saved_to_freq)}" + ) + +input = open("input.txt").read().strip() + + +def sum_keys_over_x(d: dict[int, int], x: int) -> int: + total: int = 0 + for k, v in d.items(): + if k >= x: + total += v + return total + + +part1_result = day20(input, 2) + +print( + f"part1 actual: {sum_keys_over_x(part1_result, 100)} cheats save 100+ picosecs want 1422" +) + +part2_example_result: dict[int, int] = { + 50: 32, + 52: 31, + 54: 29, + 56: 39, + 58: 25, + 60: 23, + 62: 20, + 64: 19, + 66: 12, + 68: 14, + 70: 12, + 72: 22, + 74: 4, + 76: 3, +} + +part2_result = day20(example, 20) +part2_result_50 = {} +for k, v in part2_example_result.items(): + if k >= 50: + part2_result_50[k] = v + +print(f"part2 example: {part2_result_50 == part2_example_result} want True") +if part2_result_50 != part2_example_result: + print( + f"DEBUG part2 example 50+ saved: {part2_result_50} want {part2_example_result}" + ) + +part2_result = day20(input, 20) +print(f"part2 actual: {sum_keys_over_x(part2_result, 100)} want 1009299") diff --git a/2024/day21/day21.py b/2024/day21/day21.py new file mode 100644 index 0000000..3203f9f --- /dev/null +++ b/2024/day21/day21.py @@ -0,0 +1,216 @@ +num_pad: dict[str, tuple[int, int]] = { + "7": (0, 0), + "8": (0, 1), + "9": (0, 2), + "4": (1, 0), + "5": (1, 1), + "6": (1, 2), + "1": (2, 0), + "2": (2, 1), + "3": (2, 2), + "0": (3, 1), + "A": (3, 2), + "EMPTY": (3, 0), +} + +dir_pad: dict[str, tuple[int, int]] = { + "^": (0, 1), + "A": (0, 2), + "<": (1, 0), + "v": (1, 1), + ">": (1, 2), + "EMPTY": (0, 0), +} + + +# had to manually memo because the pad dict type isn't hashable... i'm sure +# there's a way around this to use functools.cache like others... +def get_optimal_path_length( + pad: dict[str, tuple[int, int]], + start: str, + end: str, + depth: int, + memo: dict[str, int], +) -> int: + key = f"{start},{end},{depth},{pad is dir_pad}" + if key in memo: + return memo[key] + + # determine horizontal and vertical parts of path + # recurse in each direction and return whichever is shorter + start_row, start_col = pad[start] + end_row, end_col = pad[end] + + dr: int = end_row - start_row + dc: int = end_col - start_col + + horiz: str = "" + vert: str = "" + if dc > 0: + horiz = ">" * dc + else: + horiz = "<" * -dc + + if dr > 0: + vert = "v" * dr + else: + vert = "^" * -dr + + horiz_first = "A" + horiz + vert + "A" + vert_first = "A" + vert + horiz + "A" + + if depth == 0: + memo[key] = len(horiz_first[1:]) + return memo[key] + + horiz_first_result: int = 0 + for i in range(len(horiz_first) - 1): + horiz_first_result += get_optimal_path_length( + dir_pad, horiz_first[i], horiz_first[i + 1], depth - 1, memo + ) + + vert_first_result: int = 0 + for i in range(len(vert_first) - 1): + vert_first_result += get_optimal_path_length( + dir_pad, vert_first[i], vert_first[i + 1], depth - 1, memo + ) + + # still need to do the recursive calls, THEN decide which to return + # no horizontal or vertical movement so obviously just need to do the move plus "A" + if horiz == "" or vert == "": + assert horiz_first_result == vert_first_result + memo[key] = horiz_first_result + return horiz_first_result + + # if it could hit the empty spot, just return the opposite direction first + # moving horizontally + if (start_row, end_col) == pad["EMPTY"]: + memo[key] = vert_first_result + return vert_first_result + # moving vertically + if (end_row, start_col) == pad["EMPTY"]: + memo[key] = horiz_first_result + return horiz_first_result + + if horiz_first_result < vert_first_result: + memo[key] = horiz_first_result + return horiz_first_result + memo[key] = vert_first_result + return vert_first_result + + +def part1(input: str, depth: int) -> int: + + ans: int = 0 + for line in input.splitlines(): + # starts at bottom right A button so add one onto the front of the line + line = "A" + line + + path: int = 0 + for i in range(len(line) - 1): + best_path = get_optimal_path_length( + num_pad, line[i], line[i + 1], depth, {} + ) + path += best_path + + # length of shortest sequence * numeric part of input + # all are the same so just remove A char and convert to int + ans += int(line[1:4]) * path + return ans + + +example = """029A +980A +179A +456A +379A""" + +input = open("input.txt").read().strip() + +print(f"part1 example: {part1(example, 2)} want 126384") +print(f"part1 actual: {part1(input, 2)} want 237342") +print(f"part1 actual: {part1(input, 25)} want 237342") + +# +---+---+---+ +# | 7 | 8 | 9 | +# +---+---+---+ +# | 4 | 5 | 6 | +# +---+---+---+ +# | 1 | 2 | 3 | +# +---+---+---+ +# | 0 | A | +# +---+---+ + + +# 3 of these robots chained together to type into the pad above... +# +---+---+ +# | ^ | A | +# +---+---+---+ +# | < | v | > | +# +---+---+---+ + + +# unfortunately had to scrap this method because memoizing the entire resulting +# string takes up way too much memory and that nukes the runtime... +# refactoring to just store the actual length is plenty... and possible to memo +def get_optimal_path( + pad: dict[str, tuple[int, int]], + start: str, + end: str, + depth: int, +) -> str: + # determine horizontal and vertical parts of path + # recurse in each direction and return whichever is shorter + start_row, start_col = pad[start] + end_row, end_col = pad[end] + + dr: int = end_row - start_row + dc: int = end_col - start_col + + horiz: str = "" + vert: str = "" + if dc > 0: + horiz = ">" * dc + else: + horiz = "<" * -dc + + if dr > 0: + vert = "v" * dr + else: + vert = "^" * -dr + + horiz_first = "A" + horiz + vert + "A" + vert_first = "A" + vert + horiz + "A" + + if depth == 0: + return horiz_first[1:] + + horiz_first_result: str = "" + for i in range(len(horiz_first) - 1): + horiz_first_result += get_optimal_path( + dir_pad, horiz_first[i], horiz_first[i + 1], depth - 1 + ) + + vert_first_result: str = "" + for i in range(len(vert_first) - 1): + vert_first_result += get_optimal_path( + dir_pad, vert_first[i], vert_first[i + 1], depth - 1 + ) + + # still need to do the recursive calls, THEN decide which to return + # no horizontal or vertical movement so obviously just need to do the move plus "A" + if horiz == "" or vert == "": + assert horiz_first_result == vert_first_result + return horiz_first_result + + # if it could hit the empty spot, just return the opposite direction first + # moving horizontally + if (start_row, end_col) == pad["EMPTY"]: + return vert_first_result + # moving vertically + if (end_row, start_col) == pad["EMPTY"]: + return horiz_first_result + + if len(horiz_first_result) < len(vert_first_result): + return horiz_first_result + return vert_first_result diff --git a/2024/day22/day22.py b/2024/day22/day22.py new file mode 100644 index 0000000..e2cbf09 --- /dev/null +++ b/2024/day22/day22.py @@ -0,0 +1,58 @@ +from collections import defaultdict + + +def day22(input: str, part: int) -> int: + total: int = 0 + + last_four_diffs_to_price: dict[tuple[int, int, int, int], int] = defaultdict(int) + + for line in input.splitlines(): + nums: list[int] = [int(line)] + diffs: list[int] = [] + + seen_last_fours: set[tuple[int, int, int, int]] = set() + for _ in range(2000): + nums.append(get_next_secret_number(nums[-1])) + diff = nums[-1] % 10 - nums[-2] % 10 + diffs.append(diff) + total += nums[-1] + + for i in range(3, len(diffs)): + last_four_diffs = (diffs[i - 3], diffs[i - 2], diffs[i - 1], diffs[i]) + # only count the first time we've seen this set of diffs, aka only sell once + if last_four_diffs in seen_last_fours: + continue + seen_last_fours.add(last_four_diffs) + + last_four_diffs_to_price[last_four_diffs] += nums[i + 1] % 10 + + if part == 1: + return total + + most_bananas = max(last_four_diffs_to_price.values()) + return most_bananas + + +def get_next_secret_number(num: int) -> int: + num = (num ^ (num * 64)) % 16777216 + num = (num ^ (num // 32)) % 16777216 + num = (num ^ (num * 2048)) % 16777216 + return num + + +example = """1 +10 +100 +2024""" + +input = open("input.txt").read().strip() + +print(f"part1 example: {day22(example, 1)} want 37327623") +print(f"part1 actual: {day22(input, 1)} want 17612566393") + +example_part_2 = """1 +2 +3 +2024""" +print(f"part2 example: {day22(example_part_2, 2)} want 23") +print(f"part2 actual: {day22(input, 2)} want 1968") diff --git a/2024/day23/day23.py b/2024/day23/day23.py new file mode 100644 index 0000000..1faa1e5 --- /dev/null +++ b/2024/day23/day23.py @@ -0,0 +1,106 @@ +from collections import defaultdict + + +def part1(input: str) -> int: + graph: dict[str, set[str]] = defaultdict(set) + for line in input.splitlines(): + parts = line.split("-") + graph[parts[0]].add(parts[1]) + graph[parts[1]].add(parts[0]) + + ans_groups: set[str] = set() + for node in graph: + if node[0] != "t": + continue + neighbors = graph[node] + for neighbor in neighbors: + for neighbor2 in neighbors: + if neighbor == neighbor2: + continue + if neighbor2 in graph[neighbor]: + group: list[str] = [node, neighbor, neighbor2] + group.sort() + + ans_groups.add(",".join(group)) + + return len(ans_groups) + + +def part2(input: str) -> str: + graph: dict[str, set[str]] = defaultdict(set) + for line in input.splitlines(): + parts = line.split("-") + graph[parts[0]].add(parts[1]) + graph[parts[1]].add(parts[0]) + + seen: set[str] = set() + largest_group: set[str] = set() + for node in graph: + if node in seen: + continue + seen.add(node) + + group: set[str] = {node} + for neighbor in graph[node]: + if check_group_against_new_node(group, graph, neighbor): + group.add(neighbor) + seen.add(neighbor) + + if len(group) > len(largest_group): + largest_group = group + + final_group: list[str] = list(largest_group) + final_group.sort() + return ",".join(final_group) + + +def check_group_against_new_node( + group: set[str], graph: dict[str, set[str]], node_to_add: str +) -> bool: + for node in group: + if node not in graph[node_to_add]: + return False + + return True + + +example = """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""" + +input = open("input.txt").read().strip() + +print(f"part1 example: {part1(example)} want 7") +print(f"part1 actual: {part1(input)} want 1485") + +print(f"part2 example: {part2(example)} want co,de,ka,ta") +print(f"part2 actual: {part2(input)} want cc,dz,ea,hj,if,it,kf,qo,sk,ug,ut,uv,wh") diff --git a/2024/day24/day24.py b/2024/day24/day24.py new file mode 100644 index 0000000..32bba38 --- /dev/null +++ b/2024/day24/day24.py @@ -0,0 +1,250 @@ +from collections import defaultdict +import copy + + +def part1(input: str) -> int: + wires, instructions = parse_input(input) + run(instructions, wires) + return get_num(wires, "z") + + +def parse_input(input: str) -> tuple[dict[str, int], list[list[str]]]: + parts = input.split("\n\n") + + wires: dict[str, int] = defaultdict(int) + + for line in parts[0].splitlines(): + wires[line[:3]] = int(line[5:]) + + instructions: list[list[str]] = [] + for line in parts[1].splitlines(): + in1, op, in2, _, out = line.split(" ") + instructions.append([in1, op, in2, out]) + + return (wires, instructions) + + +def get_num(wires: dict[str, int], char: str) -> int: + if len(char) != 1: + raise Exception("exactly one char needed for get_num") + keys: list[str] = [] + for wire in wires: + if wire[0] == char: + keys.append(wire) + keys.sort() + binary_num: str = "" + for key in reversed(keys): + binary_num += str(wires[key]) + + return int(binary_num, 2) + + +def run(instructions: list[list[str]], wires: dict[str, int]): + processed_lines: set[int] = set() + while len(processed_lines) < len(instructions): + processed_before: int = len(processed_lines) + + for i, inst in enumerate(instructions): + if i in processed_lines: + continue + in1, op, in2, out = inst + if in1 not in wires or in2 not in wires: + continue + + if op == "AND": + wires[out] = wires[in1] & wires[in2] + elif op == "OR": + wires[out] = wires[in1] | wires[in2] + elif op == "XOR": + wires[out] = wires[in1] ^ wires[in2] + + processed_lines.add(i) + + if len(processed_lines) == processed_before: + # print("no new lines processed") + return + + +# this code is way too slow, works for the examples but unsurprisingly way too +# slow for the actual input. ditching python for a go solution... +def part2(input: str) -> str: + wires, instructions = parse_input(input) + expected_sum: int = get_num(wires, "x") & get_num(wires, "y") + + def any_values_are_equal(*args: int) -> bool: + seen: set[int] = set() + for val in args: + if val in seen: + return True + seen.add(val) + return False + + # generate swaps... maybe 8 nested for loops... + for a in range(len(instructions)): + print(a) + for b in range(a + 1, len(instructions)): + for c in range(a + 1, len(instructions)): + if any_values_are_equal(b, c): + continue + for d in range(a + 1, len(instructions)): + if any_values_are_equal(b, c, d): + continue + for e in range(a + 1, len(instructions)): + if any_values_are_equal(b, c, d, e): + continue + for f in range(a + 1, len(instructions)): + if any_values_are_equal(b, c, d, e, f): + continue + for g in range(a + 1, len(instructions)): + if any_values_are_equal(b, c, d, e, f, g): + continue + for h in range(a + 1, len(instructions)): + if any_values_are_equal(b, c, d, e, f, g, h): + continue + + instructions[a][3], instructions[b][3] = ( + instructions[b][3], + instructions[a][3], + ) + instructions[c][3], instructions[d][3] = ( + instructions[d][3], + instructions[c][3], + ) + + instructions[e][3], instructions[f][3] = ( + instructions[f][3], + instructions[e][3], + ) + instructions[g][3], instructions[h][3] = ( + instructions[h][3], + instructions[g][3], + ) + + wires_copy = copy.deepcopy(wires) + run(instructions, wires_copy) + sum = get_num(wires_copy, "z") + if sum == expected_sum: + swaps: list[str] = [ + instructions[a][3], + instructions[b][3], + instructions[c][3], + instructions[d][3], + instructions[e][3], + instructions[f][3], + instructions[g][3], + instructions[h][3], + ] + swaps.sort() + return ",".join(swaps) + # backtrack + instructions[a][3], instructions[b][3] = ( + instructions[b][3], + instructions[a][3], + ) + instructions[c][3], instructions[d][3] = ( + instructions[d][3], + instructions[c][3], + ) + instructions[e][3], instructions[f][3] = ( + instructions[f][3], + instructions[e][3], + ) + instructions[g][3], instructions[h][3] = ( + instructions[h][3], + instructions[g][3], + ) + + raise Exception("should return from loop") + + +example = """x00: 1 +x01: 1 +x02: 1 +y00: 0 +y01: 1 +y02: 0 + +x00 AND y00 -> z00 +x01 XOR y01 -> z01 +x02 OR y02 -> z02""" + +print(f"part1 example: {part1(example)} want 4") + +big_example = """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""" + +print(f"part1 big_example: {part1(big_example)} want 2024") + +input = open("input.txt").read().strip() +print(f"part1 actual: {part1(input)} want 38869984335432") + +part2_example_2_pairs_swapped = """x00: 0 +x01: 1 +x02: 0 +x03: 1 +x04: 0 +x05: 1 +y00: 0 +y01: 0 +y02: 1 +y03: 1 +y04: 0 +y05: 1 + +x00 AND y00 -> z05 +x01 AND y01 -> z02 +x02 AND y02 -> z01 +x03 AND y03 -> z03 +x04 AND y04 -> z04 +x05 AND y05 -> z00""" + +# print( +# f"part2 example with two swapped {part2(part2_example_2_pairs_swapped)} want z00,z01,z02,z05" +# ) + +print(f"part2 actual: {part2(input)} want QQ") diff --git a/2024/day24/main.go b/2024/day24/main.go new file mode 100644 index 0000000..6fb2676 --- /dev/null +++ b/2024/day24/main.go @@ -0,0 +1,259 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "log" + "math/rand" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + // drg,gvw,jbp,jgc,qjb,z15,z22,z35 + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + calc := parseInput(input) + res := 0 + + cache := map[string]int{} + + for k, v := range calc.wires { + cache[k] = v + } + + for k := range calc.outputToSource { + if k[0] != 'z' { + continue + } + + val, err := getWireVal(k, calc, cache, 0) + + if err != nil { + log.Fatalf("Did not expect error\n") + } + + if val == 0 { + continue + } + + i := cast.ToInt(k[1:]) + + res += 1 << int(i) + } + + return res +} + +func part2(input string) string { + calc := parseInput(input) + + swaps := map[string]bool{} + ans := map[string]bool{} + + fmt.Println("this will take a while to run") + backtrack(0, calc, swaps, ans) + + wires := []string{} + for wire := range ans { + wires = append(wires, wire) + } + + sort.Strings(wires) + + return strings.Join(wires, ",") +} + +func getWireVal(wire string, calc Calculator, cache map[string]int, depth int) (int, error) { + if val, ok := cache[wire]; ok { + return val, nil + } + + if depth > 200 { + return -1, fmt.Errorf("likely a loop, exit") + } + + op, ok := calc.outputToSource[wire] + + if !ok { + return -1, fmt.Errorf("cannot get value for wire %v", wire) + } + + v1, _ := getWireVal(op.inputs[0], calc, cache, depth+1) + v2, _ := getWireVal(op.inputs[1], calc, cache, depth+1) + + val := 0 + if op.op == "AND" { + val = v1 & v2 + } else if op.op == "OR" { + val = v1 | v2 + } else if op.op == "XOR" { + val = v1 ^ v2 + } + + cache[wire] = val + + return val, nil +} + +func getFirstWrong(calc Calculator) (int, map[string]int, error) { + cRes := map[string]int{} + bRes := 100 + + for range 10 { + cache := map[string]int{} + + carry := 0 + for b := range 45 { + xStr := fmt.Sprintf("x%02d", b) + yStr := fmt.Sprintf("y%02d", b) + zStr := fmt.Sprintf("z%02d", b) + + cache[xStr] = rand.Intn(2) + cache[yStr] = rand.Intn(2) + + x := cache[xStr] + y := cache[yStr] + + zCalc, err := getWireVal(zStr, calc, cache, 0) + + if err != nil { + return -1, nil, err + } + + zExp := (x + y + carry) % 2 + carry = (x + y + carry) / 2 + + if zCalc != zExp && b < bRes { + bRes = b + cRes = cache + break + } + } + } + + if bRes == 100 { + bRes = -1 + } + + return bRes, cRes, nil +} + +func checkCalc(calc Calculator) bool { + bErr, _, err := getFirstWrong(calc) + + return bErr == -1 && err == nil +} + +func backtrack(b int, calc Calculator, swaps map[string]bool, ans map[string]bool) { + if len(ans) > 0 { + return + } + // fmt.Printf("Checking %v with swaps\n%v\n", b, swaps) + if len(swaps) == 8 { + if checkCalc(calc) { + for k := range swaps { + ans[k] = true + } + } + + return + } + + bErr, conns, err := getFirstWrong(calc) + + if bErr < b || err != nil { + return + } + + conns2 := map[string]Connections{} + + for k, v := range calc.outputToSource { + conns2[k] = v + } + + for c1 := range conns { + if swaps[c1] || c1[0] == 'x' || c1[0] == 'y' { + continue + } + + for c2 := range conns2 { + if c2 == c1 || swaps[c2] || c2[0] == 'x' || c2[0] == 'y' { + continue + } + + calc.outputToSource[c1], calc.outputToSource[c2] = calc.outputToSource[c2], calc.outputToSource[c1] + + swaps[c1] = true + swaps[c2] = true + + backtrack(bErr+1, calc, swaps, ans) + + delete(swaps, c2) + delete(swaps, c1) + + calc.outputToSource[c1], calc.outputToSource[c2] = calc.outputToSource[c2], calc.outputToSource[c1] + } + } +} + +type Connections struct { + inputs []string + op string +} + +type Calculator struct { + outputToSource map[string]Connections + wires map[string]int +} + +func parseInput(input string) Calculator { + parts := strings.Split(input, "\n\n") + + wires := map[string]int{} + for _, line := range strings.Split(parts[0], "\n") { + wires[line[:3]] = cast.ToInt(line[5:]) + } + + outputToSource := map[string]Connections{} + for _, line := range strings.Split(parts[1], "\n") { + lineParts := strings.Split(line, " ") + in1, op, in2, out := lineParts[0], lineParts[1], lineParts[2], lineParts[4] + outputToSource[out] = Connections{ + inputs: []string{in1, in2}, + op: op, + } + } + + return Calculator{outputToSource, wires} +} diff --git a/2024/day25/day25.py b/2024/day25/day25.py new file mode 100644 index 0000000..289fbb7 --- /dev/null +++ b/2024/day25/day25.py @@ -0,0 +1,88 @@ +def part1(input: str) -> int: + parts = input.split("\n\n") + + locks: list[list[int]] = [] + keys: list[list[int]] = [] + max_height: int = 0 + for part in parts: + lines = part.split("\n") + max_height = len(lines) - 1 + + # lock + if lines[0] == "#" * len(lines[0]): + lock_heights: list[int] = [] + for col in range(len(lines[0])): + for row in reversed(range(len(lines))): + if lines[row][col] != ".": + lock_heights.append(row) + break + locks.append(lock_heights) + else: + # key + key_heights: list[int] = [] + for col in range(len(lines[0])): + for row in range(len(lines)): + if lines[row][col] != ".": + key_heights.append(len(lines) - 1 - row) + break + keys.append(key_heights) + + count: int = 0 + + for lock in locks: + for key in keys: + fits: bool = True + for col in range(len(key)): + if lock[col] + key[col] >= max_height: + fits = False + break + if fits: + count += 1 + + return count + + +example = """##### +.#### +.#### +.#### +.#.#. +.#... +..... + +##### +##.## +.#.## +...## +...#. +...#. +..... + +..... +#.... +#.... +#...# +#.#.# +#.### +##### + +..... +..... +#.#.. +###.. +###.# +###.# +##### + +..... +..... +..... +#.... +#.#.. +#.#.# +#####""" + +print(f"part1 example: {part1(example)} want 3") + +input = open("input.txt").read().strip() +print(f"part1 actual: {part1(input)} want 3114") diff --git a/2024/day25/main.go b/2024/day25/main.go new file mode 100644 index 0000000..a661793 --- /dev/null +++ b/2024/day25/main.go @@ -0,0 +1,91 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) +} + +func part1(input string) int { + parts := strings.Split(input, "\n\n") + + locks := [][]int{} + keys := [][]int{} + maxHeight := 0 + for _, part := range parts { + lines := strings.Split(part, "\n") + maxHeight = len(lines) - 1 + + // lock detection: first line is all '#' + fullLock := strings.Repeat("#", len(lines[0])) + if lines[0] == fullLock { + lockHeights := []int{} + // for each column, find first non-dot from bottom + for col := range len(lines[0]) { + for row := len(lines) - 1; row >= 0; row-- { + if lines[row][col:col+1] != "." { + lockHeights = append(lockHeights, row) + break + } + } + } + locks = append(locks, lockHeights) + } else { + // key detection: for each column, find first non-dot from top + keyHeights := []int{} + for col := range len(lines[0]) { + for row := range len(lines) { + if lines[row][col:col+1] != "." { + keyHeights = append(keyHeights, len(lines)-1-row) + break + } + } + } + keys = append(keys, keyHeights) + } + } + + count := 0 + + for _, lock := range locks { + for _, key := range keys { + fits := true + for col := range len(key) { + if lock[col]+key[col] >= maxHeight { + fits = false + break + } + } + if fits { + count += 1 + } + } + } + + return count +} diff --git a/2024/day25/main_test.go b/2024/day25/main_test.go new file mode 100644 index 0000000..33ac789 --- /dev/null +++ b/2024/day25/main_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "testing" +) + +var example = `##### +.#### +.#### +.#### +.#.#. +.#... +..... + +##### +##.## +.#.## +...## +...#. +...#. +..... + +..... +#.... +#.... +#...# +#.#.# +#.### +##### + +..... +..... +#.#.. +###.. +###.# +###.# +##### + +..... +..... +..... +#.... +#.#.. +#.#.# +#####` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 3, + }, + { + name: "actual", + input: input, + want: 3114, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2025/day01/main.go b/2025/day01/main.go new file mode 100644 index 0000000..156f0ec --- /dev/null +++ b/2025/day01/main.go @@ -0,0 +1,107 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + instructions := strings.Split(input, "\n") + + val := 50 + zeroes := 0 + for _, inst := range instructions { + dir := inst[:1] + num := cast.ToInt(inst[1:]) + + if dir == "R" { + val += num + val %= 100 + } else if dir == "L" { + val -= num + for val < 0 { + val += 100 + } + val %= 100 // is this even necessary? + } else { + panic(fmt.Sprintf("unexpected dir: %q", dir)) + } + if val == 0 { + zeroes++ + } + } + + return zeroes +} + +func part2(input string) int { + val := 50 + zeroes := 0 + for _, inst := range strings.Split(input, "\n") { + dir := inst[:1] + num := cast.ToInt(inst[1:]) + + if dir == "R" { + val += num + zeroes += val / 100 + val %= 100 + } else if dir == "L" { + wasZero := val == 0 + val -= num + + // special case when lands on zero because for loop will undercount by 1 + landedOnZero := val%100 == 0 + if landedOnZero { + zeroes++ + } + + for val < 0 { + val += 100 + zeroes++ + } + + // if it started at zero and turned left, the for loop above will over count by 1 + // 0 -> L5 = -5 + 100 (zeroes++ incorrectly) + if wasZero { + zeroes-- + } + } else { + panic(fmt.Sprintf("unexpected dir: %q", dir)) + } + } + + return zeroes +} diff --git a/2025/day01/main_test.go b/2025/day01/main_test.go new file mode 100644 index 0000000..b390298 --- /dev/null +++ b/2025/day01/main_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "testing" +) + +var example = `L68 +L30 +R48 +L5 +R60 +L55 +L1 +L99 +R14 +L82` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 3, + }, + { + name: "actual", + input: input, + want: 1084, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 6, + }, + { + name: "L50", + input: "L50", + want: 1, + }, + { + name: "R50", + input: "R50", + want: 1, + }, + { + name: "L50R200", + input: "L50\nR200", + want: 3, + }, + { + // ugh off by ones in the "L" branch + name: "L50L200", + input: "L50\nL200", + want: 3, + }, + { + name: "actual", + input: input, + want: 6475, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2025/day02/main.go b/2025/day02/main.go new file mode 100644 index 0000000..6732040 --- /dev/null +++ b/2025/day02/main.go @@ -0,0 +1,98 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + pairs := parseInput(input) + + total := 0 + for _, p := range pairs { + for i := p[0]; i <= p[1]; i++ { + str := cast.ToString(i) + // check str is even length (probably not necessary...) + if len(str)%2 != 0 { + continue + } + // directly check first half to second half of str + if str[:len(str)/2] == str[len(str)/2:] { + total += i + } + } + } + + return total +} + +func part2(input string) int { + pairs := parseInput(input) + + total := 0 + for _, p := range pairs { + for i := p[0]; i <= p[1]; i++ { + str := cast.ToString(i) + + for l := 1; l <= len(str)/2; l++ { + // skip if this chunk size does not divide evenly into the entire str + if len(str)%l != 0 { + continue + } + + // compare chunk repeated the correct number of times against the entire str + chunk := str[:l] + if str == strings.Repeat(chunk, len(str)/l) { + total += i + break + } + } + } + } + + return total +} + +func parseInput(input string) (ans [][]int) { + for _, line := range strings.Split(input, ",") { + nums := strings.Split(line, "-") + ans = append(ans, []int{ + cast.ToInt(nums[0]), + cast.ToInt(nums[1]), + }) + } + return ans +} diff --git a/2025/day02/main_test.go b/2025/day02/main_test.go new file mode 100644 index 0000000..2fde32c --- /dev/null +++ b/2025/day02/main_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "testing" +) + +var example = `11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 1227775554, + }, + { + name: "actual", + input: input, + want: 30599400849, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 4174379265, + }, + { + name: "actual", + input: input, + want: 46270373595, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/350.png b/350.png deleted file mode 100644 index 51731c7..0000000 Binary files a/350.png and /dev/null differ diff --git a/500.png b/500.png new file mode 100644 index 0000000..0c11f22 Binary files /dev/null and b/500.png differ diff --git a/README.md b/README.md index fae27aa..1158d6e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ -![350 stars!](./350.png) +![500 stars!](./500.png) -[reddit thread after completing the first six years](https://www.reddit.com/r/adventofcode/comments/klzgnx/complete_repo_and_thoughts_in_comments/) +### Quick Note +1. I started this in a pre-generics Go/Golang world. Maybe one day I'll come back and learn generics as they'd be quite useful here. But that's for future me. +2. I decided to do 2024 in Python to get used to it for a new job, I have every intention of filling in my solutions in Go... but who knows how long that TODO will last... -[reddit thread post 2021](https://www.reddit.com/r/adventofcode/comments/rrog0y/all_caught_up_repo_all_gogolang_thoughts_in/) ## Running Locally ### Requirements Go 1.16+ is required because [embed][embed] is used for input files. +Go 1.22 is needed for some features like ranging over ints or built in min() and max(). I started using these for the 2023 solutions iirc. Use `go run main.go -part <1 or 2>` will be usable to run the actual inputs for that day. diff --git a/go.mod b/go.mod index 097674e..c2980bb 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/Threnklyn/advent-of-code-go -go 1.16 +go 1.22 require golang.org/x/net v0.0.0-20201110031124-69a78807bb2b diff --git a/mathy/lcm.go b/mathy/lcm.go new file mode 100644 index 0000000..4394439 --- /dev/null +++ b/mathy/lcm.go @@ -0,0 +1,18 @@ +package mathy + +func LeastCommonMultiple(i, j int) int { + gcd := GreatestCommonMultiple(i, j) + + return (i * j) / gcd +} + +func GreatestCommonMultiple(i, j int) int { + if j > i { + i, j = j, i + } + + if j == 0 { + return i + } + return GreatestCommonMultiple(j, i%j) +}