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/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) + } + }) + } +}