diff --git a/README.md b/2021/README.md similarity index 52% rename from README.md rename to 2021/README.md index 32a3254..a05ff96 100644 --- a/README.md +++ b/2021/README.md @@ -2,6 +2,10 @@ ## All code for advent of code 2021 +### TODO + +- [ ] write cli for running a file like: `deno task run 1 1` + --- JoΓ«l Kuijper diff --git a/day-1/input.txt b/2021/day-1/input.txt similarity index 100% rename from day-1/input.txt rename to 2021/day-1/input.txt diff --git a/day-1/part-1/main.ts b/2021/day-1/part-1/main.ts similarity index 100% rename from day-1/part-1/main.ts rename to 2021/day-1/part-1/main.ts diff --git a/day-1/part-2/main.ts b/2021/day-1/part-2/main.ts similarity index 100% rename from day-1/part-2/main.ts rename to 2021/day-1/part-2/main.ts diff --git a/day-10/input.txt b/2021/day-10/input.txt similarity index 100% rename from day-10/input.txt rename to 2021/day-10/input.txt diff --git a/day-10/main.ts b/2021/day-10/main.ts similarity index 100% rename from day-10/main.ts rename to 2021/day-10/main.ts diff --git a/day-2/input.txt b/2021/day-2/input.txt similarity index 100% rename from day-2/input.txt rename to 2021/day-2/input.txt diff --git a/day-2/part-1/main.ts b/2021/day-2/part-1/main.ts similarity index 100% rename from day-2/part-1/main.ts rename to 2021/day-2/part-1/main.ts diff --git a/day-2/part-2/main.ts b/2021/day-2/part-2/main.ts similarity index 100% rename from day-2/part-2/main.ts rename to 2021/day-2/part-2/main.ts diff --git a/day-3/input.txt b/2021/day-3/input.txt similarity index 100% rename from day-3/input.txt rename to 2021/day-3/input.txt diff --git a/day-3/part-1/main.ts b/2021/day-3/part-1/main.ts similarity index 100% rename from day-3/part-1/main.ts rename to 2021/day-3/part-1/main.ts diff --git a/day-3/part-2/main.ts b/2021/day-3/part-2/main.ts similarity index 100% rename from day-3/part-2/main.ts rename to 2021/day-3/part-2/main.ts diff --git a/day-3/part-2/temp.ts b/2021/day-3/part-2/temp.ts similarity index 100% rename from day-3/part-2/temp.ts rename to 2021/day-3/part-2/temp.ts diff --git a/day-4/input.txt b/2021/day-4/input.txt similarity index 100% rename from day-4/input.txt rename to 2021/day-4/input.txt diff --git a/day-4/main.ts b/2021/day-4/main.ts similarity index 100% rename from day-4/main.ts rename to 2021/day-4/main.ts diff --git a/day-5/input.txt b/2021/day-5/input.txt similarity index 100% rename from day-5/input.txt rename to 2021/day-5/input.txt diff --git a/day-5/main.ts b/2021/day-5/main.ts similarity index 100% rename from day-5/main.ts rename to 2021/day-5/main.ts diff --git a/day-6/input.txt b/2021/day-6/input.txt similarity index 100% rename from day-6/input.txt rename to 2021/day-6/input.txt diff --git a/day-6/main.ts b/2021/day-6/main.ts similarity index 100% rename from day-6/main.ts rename to 2021/day-6/main.ts diff --git a/day-7/input.txt b/2021/day-7/input.txt similarity index 100% rename from day-7/input.txt rename to 2021/day-7/input.txt diff --git a/day-7/main.ts b/2021/day-7/main.ts similarity index 100% rename from day-7/main.ts rename to 2021/day-7/main.ts diff --git a/day-8/input.txt b/2021/day-8/input.txt similarity index 100% rename from day-8/input.txt rename to 2021/day-8/input.txt diff --git a/day-8/main.ts b/2021/day-8/main.ts similarity index 100% rename from day-8/main.ts rename to 2021/day-8/main.ts diff --git a/day-9/input.txt b/2021/day-9/input.txt similarity index 100% rename from day-9/input.txt rename to 2021/day-9/input.txt diff --git a/day-9/main.ts b/2021/day-9/main.ts similarity index 100% rename from day-9/main.ts rename to 2021/day-9/main.ts diff --git a/deno.json b/2021/deno.json similarity index 100% rename from deno.json rename to 2021/deno.json diff --git a/utils.ts b/2021/utils.ts similarity index 100% rename from utils.ts rename to 2021/utils.ts diff --git a/2022/.cargo/config b/2022/.cargo/config new file mode 100644 index 0000000..b8ad9a6 --- /dev/null +++ b/2022/.cargo/config @@ -0,0 +1,5 @@ +[build] +rustflags = ["-C", "target-cpu=native"] + +[alias] +rr = "run --release" diff --git a/2022/.editorconfig b/2022/.editorconfig new file mode 100644 index 0000000..c075e7e --- /dev/null +++ b/2022/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: http://EditorConfig.org +root = true + +[*] +indent_size = 4 +indent_style = space +end_of_line = lf +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.txt] +insert_final_newline = false + +[*.md] +trim_trailing_whitespace = false diff --git a/2022/.gitignore b/2022/.gitignore new file mode 100644 index 0000000..81498f2 --- /dev/null +++ b/2022/.gitignore @@ -0,0 +1,20 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + + +# Added by cargo + +/target + +# Advent of Code +# @see https://old.reddit.com/r/adventofcode/comments/k99rod/sharing_input_data_were_we_requested_not_to/gf2ukkf/?context=3 +inputs +!inputs/.keep diff --git a/2022/.vscode/launch.json b/2022/.vscode/launch.json new file mode 100644 index 0000000..919d015 --- /dev/null +++ b/2022/.vscode/launch.json @@ -0,0 +1,64 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'aoc'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=aoc", + "--package=aoc" + ], + "filter": { + "name": "aoc", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'aoc'", + "cargo": { + "args": [ + "build", + "--bin=aoc", + "--package=aoc" + ], + "filter": { + "name": "aoc", + "kind": "bin" + } + }, + "args": ["1"], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'aoc'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=aoc" + ], + "filter": { + "name": "aoc", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/2022/Cargo.lock b/2022/Cargo.lock new file mode 100644 index 0000000..a012b5f --- /dev/null +++ b/2022/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aoc" +version = "0.1.0" +dependencies = [ + "itertools", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] diff --git a/2022/Cargo.toml b/2022/Cargo.toml new file mode 100644 index 0000000..b8cff29 --- /dev/null +++ b/2022/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "aoc" +version = "0.1.0" +authors = ["Felix SpΓΆttel <1682504+fspoettel@users.noreply.github.com>"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[profile.release] +lto = "thin" + +[dependencies] +itertools = "0.10.1" diff --git a/2022/LICENSE b/2022/LICENSE new file mode 100644 index 0000000..b97fd05 --- /dev/null +++ b/2022/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Felix Spoettel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/2022/README.md b/2022/README.md new file mode 100644 index 0000000..3cf937f --- /dev/null +++ b/2022/README.md @@ -0,0 +1,121 @@ + + +# πŸŽ„ [Advent of Code](https://adventofcode.com/) + +![Language](https://badgen.net/badge/Language/Rust/orange) + + + +## 2022 Results + +| Day | Part 1 | Part 2 | +| :--------------------------------------------: | :----: | :----: | +| [Day 1](https://adventofcode.com/2021/day/1) | ⭐ | ⭐ | +| [Day 2](https://adventofcode.com/2021/day/2) | ⭐ | ⭐ | +| [Day 3](https://adventofcode.com/2021/day/3) | ⭐ | ⭐ | +| [Day 4](https://adventofcode.com/2021/day/4) | ⭐ | ⭐ | +| [Day 5](https://adventofcode.com/2021/day/5) | ⭐ | ⭐ | +| [Day 6](https://adventofcode.com/2021/day/6) | ⭐ | ⭐ | +| [Day 7](https://adventofcode.com/2021/day/7) | ⭐ | ⭐ | +| [Day 8](https://adventofcode.com/2021/day/8) | ⭐ | ⭐ | +| [Day 9](https://adventofcode.com/2021/day/9) | ⭐ | ⭐ | +| [Day 10](https://adventofcode.com/2021/day/10) | ⭐ | ⭐ | +| [Day 11](https://adventofcode.com/2021/day/11) | ⭐ | ⭐ | +| [Day 12](https://adventofcode.com/2021/day/12) | ⭐ | ⭐ | +| [Day 13](https://adventofcode.com/2021/day/13) | ⭐ | ⭐ | +| [Day 14](https://adventofcode.com/2021/day/14) | ⭐ | ⭐ | +| [Day 15](https://adventofcode.com/2021/day/15) | ⭐ | ⭐ | +| [Day 16](https://adventofcode.com/2021/day/16) | ⭐ | ⭐ | +| [Day 17](https://adventofcode.com/2021/day/17) | ⭐ | ⭐ | +| [Day 18](https://adventofcode.com/2021/day/18) | ⭐ | ⭐ | +| [Day 19](https://adventofcode.com/2021/day/19) | ⭐ | ⭐ | +| [Day 20](https://adventofcode.com/2021/day/20) | ⭐ | ⭐ | +| [Day 21](https://adventofcode.com/2021/day/21) | ⭐ | ⭐ | +| [Day 22](https://adventofcode.com/2021/day/22) | ⭐ | ⭐ | +| [Day 23](https://adventofcode.com/2021/day/23) | ⭐ | ⭐ | +| [Day 24](https://adventofcode.com/2021/day/24) | ⭐ | ⭐ | +| [Day 25](https://adventofcode.com/2021/day/25) | ⭐ | ⭐ | + + + +--- + +Generated with the [advent-of-code-rust](https://github.com/fspoettel/advent-of-code-rust) template. + +## Commands + +### Setup new day + +```sh +# example: `./scripts/scaffold.sh 1` +./scripts/scaffold.sh + +# output: +# Created module `src/solutions/day01.rs` +# Created input file `src/inputs/day01.txt` +# Created example file `src/examples/day01.txt` +# Linked new module in `src/main.rs` +# Linked new module in `src/solutions/mod.rs` +# Done! πŸŽ„ +``` + +Every solution file has _unit tests_ referencing the example input file. You can use these tests to develop and debug your solution. When editing a solution file, `rust-analyzer` will display buttons for these actions above the unit tests. + +### Download inputs for a day + +```sh +# example: `./scripts/download.sh 1` +./scripts/download.sh + +# output: +# Invoking `aoc` cli... +# Loaded session cookie from "/home/foo/.adventofcode.session". +# Downloading input for day 1, 2021... +# Saving puzzle input to "/tmp/..."... +# Done! +# Wrote input to `src/inputs/day01.txt`... +# Done! πŸŽ„ +``` + +Puzzle inputs are not checked into git. [See here](https://old.reddit.com/r/adventofcode/comments/k99rod/sharing_input_data_were_we_requested_not_to/gf2ukkf/?context=3) why. + +### Run solutions for a day + +```sh +# example: `cargo run 1` +cargo run + +# output: +# Running `target/debug/aoc 1` +# ---- +# +# πŸŽ„ Part 1 πŸŽ„ +# +# 6 (elapsed: 37.03Β΅s) +# +# πŸŽ„ Part 2 πŸŽ„ +# +# 9 (elapsed: 33.18Β΅s) +# +# ---- +``` + +To run an optimized version for benchmarking, use the `--release` flag or the alias `cargo rr `. + +### Run all solutions against example input + +```sh +cargo test +``` + +### Format code + +```sh +cargo fmt +``` + +### Lint code + +```sh +cargo clippy +``` diff --git a/2022/assets/banner.png b/2022/assets/banner.png new file mode 100644 index 0000000..36ca006 Binary files /dev/null and b/2022/assets/banner.png differ diff --git a/2022/assets/christmas_ferris.png b/2022/assets/christmas_ferris.png new file mode 100644 index 0000000..365527a Binary files /dev/null and b/2022/assets/christmas_ferris.png differ diff --git a/2022/scripts/download.sh b/2022/scripts/download.sh new file mode 100755 index 0000000..2860a0e --- /dev/null +++ b/2022/scripts/download.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -e; + +if ! command -v 'aoc' &> /dev/null +then + echo "command \`aoc\` not found. Try running \`cargo install aoc-cli\` to install it." + exit 1 +fi + +if [ ! -n "$1" ]; then + >&2 echo "Argument is required for day." + exit 1 +fi + +day=$(echo $1 | sed 's/^0*//'); +day_padded=`printf %02d $day`; + +filename="day$day_padded"; +input_path="src/inputs/$filename.txt"; + +tmp_dir=$(mktemp -d); +tmp_file_path="$tmp_dir/input"; + +aoc download --day $day --file $tmp_file_path; +cat $tmp_file_path > $input_path; +echo "Wrote input to \"$input_path\"..."; + +cat <&2 echo "Argument is required for day." + exit 1 +fi + +day=$(echo $1 | sed 's/^0*//'); +day_padded=`printf %02d $day`; + +filename="day$day_padded"; + +input_path="src/inputs/$filename.txt"; +example_path="src/examples/$filename.txt"; +module_path="src/solutions/$filename.rs"; + +touch $module_path; + +cat > $module_path < u32 { + 0 +} + +pub fn part_two(input: &str) -> u32 { + 0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", day); + assert_eq!(part_one(&input), 0); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", day); + assert_eq!(part_two(&input), 0); + } +} +EOF + +perl -pi -e "s,day,$day,g" $module_path; + +echo "Created module \"$module_path\""; + +touch $input_path; +echo "Created input file \"$input_path\""; + +touch $example_path; +echo "Created example file \"$example_path\""; + +line=" $day => solve_day!($filename, &input)," +perl -pi -le "print '$line' if(/^*.day not solved/);" "src/main.rs"; + +echo "Linked new module in \"src/main.rs\""; + +LINE="pub mod $filename;"; +FILE="src/solutions/mod.rs"; +grep -qF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE"; +echo "Linked new module in \"$FILE\""; + + +cat < 5,9 +8,0 -> 0,8 +9,4 -> 3,4 +2,2 -> 2,1 +7,0 -> 7,4 +6,4 -> 2,0 +0,9 -> 2,9 +3,4 -> 1,4 +0,0 -> 8,8 +5,5 -> 8,2 \ No newline at end of file diff --git a/2022/src/examples/day06.txt b/2022/src/examples/day06.txt new file mode 100644 index 0000000..a7af2b1 --- /dev/null +++ b/2022/src/examples/day06.txt @@ -0,0 +1 @@ +3,4,3,1,2 \ No newline at end of file diff --git a/2022/src/examples/day07.txt b/2022/src/examples/day07.txt new file mode 100644 index 0000000..2bdd92f --- /dev/null +++ b/2022/src/examples/day07.txt @@ -0,0 +1 @@ +16,1,2,0,4,2,7,1,2,14 \ No newline at end of file diff --git a/2022/src/examples/day08.txt b/2022/src/examples/day08.txt new file mode 100644 index 0000000..8614893 --- /dev/null +++ b/2022/src/examples/day08.txt @@ -0,0 +1,10 @@ +be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe +edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc +fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg +fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb +aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea +fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb +dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe +bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef +egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb +gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce \ No newline at end of file diff --git a/2022/src/examples/day09.txt b/2022/src/examples/day09.txt new file mode 100644 index 0000000..610bad9 --- /dev/null +++ b/2022/src/examples/day09.txt @@ -0,0 +1,5 @@ +2199943210 +3987894921 +9856789892 +8767896789 +9899965678 \ No newline at end of file diff --git a/2022/src/examples/day10.txt b/2022/src/examples/day10.txt new file mode 100644 index 0000000..2f182d8 --- /dev/null +++ b/2022/src/examples/day10.txt @@ -0,0 +1,10 @@ +[({(<(())[]>[[{[]{<()<>> +[(()[<>])]({[<{<<[]>>( +{([(<{}[<>[]}>{[]{[(<()> +(((({<>}<{<{<>}{[]{[]{} +[[<[([]))<([[{}[[()]]] +[{[{({}]{}}([{[{{{}}([] +{<[[]]>}<{[{[{[]{()[[[] +[<(<(<(<{}))><([]([]() +<{([([[(<>()){}]>(<<{{ +<{([{{}}[<[[[<>{}]]]>[]] \ No newline at end of file diff --git a/2022/src/examples/day11.txt b/2022/src/examples/day11.txt new file mode 100644 index 0000000..a3819c9 --- /dev/null +++ b/2022/src/examples/day11.txt @@ -0,0 +1,10 @@ +5483143223 +2745854711 +5264556173 +6141336146 +6357385478 +4167524645 +2176841721 +6882881134 +4846848554 +5283751526 \ No newline at end of file diff --git a/2022/src/examples/day12.txt b/2022/src/examples/day12.txt new file mode 100644 index 0000000..da6e083 --- /dev/null +++ b/2022/src/examples/day12.txt @@ -0,0 +1,18 @@ +fs-end +he-DX +fs-he +start-DX +pj-DX +end-zg +zg-sl +zg-pj +pj-he +RW-he +fs-DX +pj-RW +zg-RW +start-pj +he-WI +zg-he +pj-fs +start-RW \ No newline at end of file diff --git a/2022/src/examples/day13.txt b/2022/src/examples/day13.txt new file mode 100644 index 0000000..32a8563 --- /dev/null +++ b/2022/src/examples/day13.txt @@ -0,0 +1,21 @@ +6,10 +0,14 +9,10 +0,3 +10,4 +4,11 +6,0 +6,12 +4,1 +0,13 +10,12 +3,4 +3,0 +8,4 +1,10 +2,14 +8,10 +9,0 + +fold along y=7 +fold along x=5 \ No newline at end of file diff --git a/2022/src/examples/day14.txt b/2022/src/examples/day14.txt new file mode 100644 index 0000000..6c1c3a1 --- /dev/null +++ b/2022/src/examples/day14.txt @@ -0,0 +1,18 @@ +NNCB + +CH -> B +HH -> N +CB -> H +NH -> C +HB -> C +HC -> B +HN -> C +NN -> C +BH -> H +NC -> B +NB -> B +BN -> B +BB -> N +BC -> B +CC -> N +CN -> C \ No newline at end of file diff --git a/2022/src/examples/day15.txt b/2022/src/examples/day15.txt new file mode 100644 index 0000000..7d9d562 --- /dev/null +++ b/2022/src/examples/day15.txt @@ -0,0 +1,10 @@ +1163751742 +1381373672 +2136511328 +3694931569 +7463417111 +1319128137 +1359912421 +3125421639 +1293138521 +2311944581 \ No newline at end of file diff --git a/2022/src/examples/day17.txt b/2022/src/examples/day17.txt new file mode 100644 index 0000000..f40609b --- /dev/null +++ b/2022/src/examples/day17.txt @@ -0,0 +1 @@ +target area: x=20..30, y=-10..-5 \ No newline at end of file diff --git a/2022/src/examples/day18.txt b/2022/src/examples/day18.txt new file mode 100644 index 0000000..2efedbf --- /dev/null +++ b/2022/src/examples/day18.txt @@ -0,0 +1,10 @@ +[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]] +[[[5,[2,8]],4],[5,[[9,9],0]]] +[6,[[[6,2],[5,6]],[[7,6],[4,7]]]] +[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]] +[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]] +[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]] +[[[[5,4],[7,7]],8],[[8,3],8]] +[[9,3],[[9,9],[6,[4,9]]]] +[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]] +[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]] \ No newline at end of file diff --git a/2022/src/examples/day19.txt b/2022/src/examples/day19.txt new file mode 100644 index 0000000..b596cc4 --- /dev/null +++ b/2022/src/examples/day19.txt @@ -0,0 +1,136 @@ +--- scanner 0 --- +404,-588,-901 +528,-643,409 +-838,591,734 +390,-675,-793 +-537,-823,-458 +-485,-357,347 +-345,-311,381 +-661,-816,-575 +-876,649,763 +-618,-824,-621 +553,345,-567 +474,580,667 +-447,-329,318 +-584,868,-557 +544,-627,-890 +564,392,-477 +455,729,728 +-892,524,684 +-689,845,-530 +423,-701,434 +7,-33,-71 +630,319,-379 +443,580,662 +-789,900,-551 +459,-707,401 + +--- scanner 1 --- +686,422,578 +605,423,415 +515,917,-361 +-336,658,858 +95,138,22 +-476,619,847 +-340,-569,-846 +567,-361,727 +-460,603,-452 +669,-402,600 +729,430,532 +-500,-761,534 +-322,571,750 +-466,-666,-811 +-429,-592,574 +-355,545,-477 +703,-491,-529 +-328,-685,520 +413,935,-424 +-391,539,-444 +586,-435,557 +-364,-763,-893 +807,-499,-711 +755,-354,-619 +553,889,-390 + +--- scanner 2 --- +649,640,665 +682,-795,504 +-784,533,-524 +-644,584,-595 +-588,-843,648 +-30,6,44 +-674,560,763 +500,723,-460 +609,671,-379 +-555,-800,653 +-675,-892,-343 +697,-426,-610 +578,704,681 +493,664,-388 +-671,-858,530 +-667,343,800 +571,-461,-707 +-138,-166,112 +-889,563,-600 +646,-828,498 +640,759,510 +-630,509,768 +-681,-892,-333 +673,-379,-804 +-742,-814,-386 +577,-820,562 + +--- scanner 3 --- +-589,542,597 +605,-692,669 +-500,565,-823 +-660,373,557 +-458,-679,-417 +-488,449,543 +-626,468,-788 +338,-750,-386 +528,-832,-391 +562,-778,733 +-938,-730,414 +543,643,-506 +-524,371,-870 +407,773,750 +-104,29,83 +378,-903,-323 +-778,-728,485 +426,699,580 +-438,-605,-362 +-469,-447,-387 +509,732,623 +647,635,-688 +-868,-804,481 +614,-800,639 +595,780,-596 + +--- scanner 4 --- +727,592,562 +-293,-554,779 +441,611,-461 +-714,465,-776 +-743,427,-804 +-660,-479,-426 +832,-632,460 +927,-485,-438 +408,393,-506 +466,436,-512 +110,16,151 +-258,-428,682 +-393,719,612 +-211,-452,876 +808,-476,-593 +-575,615,604 +-485,667,467 +-680,325,-822 +-627,-443,-432 +872,-547,-609 +833,512,582 +807,604,487 +839,-516,451 +891,-625,532 +-652,-548,-490 +30,-46,-14 \ No newline at end of file diff --git a/2022/src/examples/day20.txt b/2022/src/examples/day20.txt new file mode 100644 index 0000000..000a554 --- /dev/null +++ b/2022/src/examples/day20.txt @@ -0,0 +1,7 @@ +..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..# + +#..#. +#.... +##..# +..#.. +..### \ No newline at end of file diff --git a/2022/src/examples/day21.txt b/2022/src/examples/day21.txt new file mode 100644 index 0000000..3f69194 --- /dev/null +++ b/2022/src/examples/day21.txt @@ -0,0 +1,2 @@ +Player 1 starting position: 4 +Player 2 starting position: 8 diff --git a/2022/src/examples/day22.txt b/2022/src/examples/day22.txt new file mode 100644 index 0000000..2790bed --- /dev/null +++ b/2022/src/examples/day22.txt @@ -0,0 +1,60 @@ +on x=-5..47,y=-31..22,z=-19..33 +on x=-44..5,y=-27..21,z=-14..35 +on x=-49..-1,y=-11..42,z=-10..38 +on x=-20..34,y=-40..6,z=-44..1 +off x=26..39,y=40..50,z=-2..11 +on x=-41..5,y=-41..6,z=-36..8 +off x=-43..-33,y=-45..-28,z=7..25 +on x=-33..15,y=-32..19,z=-34..11 +off x=35..47,y=-46..-34,z=-11..5 +on x=-14..36,y=-6..44,z=-16..29 +on x=-57795..-6158,y=29564..72030,z=20435..90618 +on x=36731..105352,y=-21140..28532,z=16094..90401 +on x=30999..107136,y=-53464..15513,z=8553..71215 +on x=13528..83982,y=-99403..-27377,z=-24141..23996 +on x=-72682..-12347,y=18159..111354,z=7391..80950 +on x=-1060..80757,y=-65301..-20884,z=-103788..-16709 +on x=-83015..-9461,y=-72160..-8347,z=-81239..-26856 +on x=-52752..22273,y=-49450..9096,z=54442..119054 +on x=-29982..40483,y=-108474..-28371,z=-24328..38471 +on x=-4958..62750,y=40422..118853,z=-7672..65583 +on x=55694..108686,y=-43367..46958,z=-26781..48729 +on x=-98497..-18186,y=-63569..3412,z=1232..88485 +on x=-726..56291,y=-62629..13224,z=18033..85226 +on x=-110886..-34664,y=-81338..-8658,z=8914..63723 +on x=-55829..24974,y=-16897..54165,z=-121762..-28058 +on x=-65152..-11147,y=22489..91432,z=-58782..1780 +on x=-120100..-32970,y=-46592..27473,z=-11695..61039 +on x=-18631..37533,y=-124565..-50804,z=-35667..28308 +on x=-57817..18248,y=49321..117703,z=5745..55881 +on x=14781..98692,y=-1341..70827,z=15753..70151 +on x=-34419..55919,y=-19626..40991,z=39015..114138 +on x=-60785..11593,y=-56135..2999,z=-95368..-26915 +on x=-32178..58085,y=17647..101866,z=-91405..-8878 +on x=-53655..12091,y=50097..105568,z=-75335..-4862 +on x=-111166..-40997,y=-71714..2688,z=5609..50954 +on x=-16602..70118,y=-98693..-44401,z=5197..76897 +on x=16383..101554,y=4615..83635,z=-44907..18747 +off x=-95822..-15171,y=-19987..48940,z=10804..104439 +on x=-89813..-14614,y=16069..88491,z=-3297..45228 +on x=41075..99376,y=-20427..49978,z=-52012..13762 +on x=-21330..50085,y=-17944..62733,z=-112280..-30197 +on x=-16478..35915,y=36008..118594,z=-7885..47086 +off x=-98156..-27851,y=-49952..43171,z=-99005..-8456 +off x=2032..69770,y=-71013..4824,z=7471..94418 +on x=43670..120875,y=-42068..12382,z=-24787..38892 +off x=37514..111226,y=-45862..25743,z=-16714..54663 +off x=25699..97951,y=-30668..59918,z=-15349..69697 +off x=-44271..17935,y=-9516..60759,z=49131..112598 +on x=-61695..-5813,y=40978..94975,z=8655..80240 +off x=-101086..-9439,y=-7088..67543,z=33935..83858 +off x=18020..114017,y=-48931..32606,z=21474..89843 +off x=-77139..10506,y=-89994..-18797,z=-80..59318 +off x=8476..79288,y=-75520..11602,z=-96624..-24783 +on x=-47488..-1262,y=24338..100707,z=16292..72967 +off x=-84341..13987,y=2429..92914,z=-90671..-1318 +off x=-37810..49457,y=-71013..-7894,z=-105357..-13188 +off x=-27365..46395,y=31009..98017,z=15428..76570 +off x=-70369..-16548,y=22648..78696,z=-1892..86821 +on x=-53470..21291,y=-120233..-33476,z=-44150..38147 +off x=-93533..-4276,y=-16170..68771,z=-104985..-24507 \ No newline at end of file diff --git a/2022/src/examples/day24.txt b/2022/src/examples/day24.txt new file mode 100644 index 0000000..b5b5961 --- /dev/null +++ b/2022/src/examples/day24.txt @@ -0,0 +1,11 @@ +inp w +add z w +mod z 2 +div w 2 +add y w +mod y 2 +div w 2 +add x w +mod x 2 +div w 2 +mod w 2 \ No newline at end of file diff --git a/2022/src/examples/day25.txt b/2022/src/examples/day25.txt new file mode 100644 index 0000000..73a37cc --- /dev/null +++ b/2022/src/examples/day25.txt @@ -0,0 +1,9 @@ +v...>>.vv> +.vv>>.vv.. +>>.>v>...v +>>v>>.>.v. +v>v.vv.v.. +>.>>..v... +.vv..>.>v. +v.v..>>v.v +....v..v.> \ No newline at end of file diff --git a/2022/src/helpers/grid.rs b/2022/src/helpers/grid.rs new file mode 100644 index 0000000..1448977 --- /dev/null +++ b/2022/src/helpers/grid.rs @@ -0,0 +1,63 @@ +use std::fmt::Debug; + +/// A point describes a location `x, y` in a grid with two axis. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Point(pub usize, pub usize); + +impl Point { + /// Get a unique id for a point in a given grid. + pub fn to_id(self, width: usize) -> usize { + self.0 + width * self.1 + } + + /// Get a point from a unique id in a grid. + pub fn from_id(id: usize, width: usize) -> Self { + Point(id % width, id / width) + } + + /// Get all neighbors for a point in a grid, respecting the boundaries of the input. + pub fn neighbors(self, max_x: usize, max_y: usize, include_diagonals: bool) -> Vec { + let mut neighbors: Vec = Vec::new(); + let Point(x, y) = self; + + let bound_top = y == 0; + let bound_left = x == 0; + + let bound_bottom = y == max_y; + let bound_right = x == max_x; + + if !bound_top { + neighbors.push(Point(x, y - 1)); + + if include_diagonals && !bound_left { + neighbors.push(Point(x - 1, y - 1)); + } + + if include_diagonals && !bound_right { + neighbors.push(Point(x + 1, y - 1)); + } + } + + if !bound_bottom { + neighbors.push(Point(x, y + 1)); + + if include_diagonals && !bound_left { + neighbors.push(Point(x - 1, y + 1)); + } + + if include_diagonals && !bound_right { + neighbors.push(Point(x + 1, y + 1)); + } + } + + if !bound_left { + neighbors.push(Point(x - 1, y)); + } + + if !bound_right { + neighbors.push(Point(x + 1, y)); + } + + neighbors + } +} diff --git a/2022/src/helpers/math.rs b/2022/src/helpers/math.rs new file mode 100644 index 0000000..b9ad938 --- /dev/null +++ b/2022/src/helpers/math.rs @@ -0,0 +1,37 @@ +/// get a vector's median value. +/// the median is the value separating the higher half from the lower half of a data sample. +/// [Wikipedia](https://en.wikipedia.org/wiki/Median) +pub fn median(vec: &mut Vec) -> u64 { + let len = vec.len(); + let mid = len / 2; + + vec.sort_unstable(); + + if len % 2 == 0 { + (vec[mid - 1] + vec[mid]) / 2 + } else { + vec[mid] + } +} + +/// sum a sequence of integers. (e.g. `1, 2, 3, 4`) +/// [Wikipedia](https://en.wikipedia.org/wiki/Triangular_number) +pub fn nth_triangular(a: u64) -> u64 { + a * (a + 1) / 2 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_median() { + assert_eq!(median(&mut [1, 4, 7].to_vec()), 4); + assert_eq!(median(&mut [3, 10, 36, 255, 79, 24, 5, 8].to_vec()), 17); + } + + #[test] + fn test_nth_triangular() { + assert_eq!(nth_triangular(7), 28); + } +} diff --git a/2022/src/helpers/mod.rs b/2022/src/helpers/mod.rs new file mode 100644 index 0000000..e25af20 --- /dev/null +++ b/2022/src/helpers/mod.rs @@ -0,0 +1,2 @@ +pub mod grid; +pub mod math; diff --git a/2022/src/lib.rs b/2022/src/lib.rs new file mode 100644 index 0000000..4696f5c --- /dev/null +++ b/2022/src/lib.rs @@ -0,0 +1,14 @@ +use std::env; +use std::fs; + +pub fn read_file(folder: &str, day: u8) -> String { + let cwd = env::current_dir().unwrap(); + + let filepath = cwd + .join("src") + .join(folder) + .join(format!("day{:02}.txt", day)); + + let f = fs::read_to_string(filepath); + f.expect("could not open input file") +} diff --git a/2022/src/main.rs b/2022/src/main.rs new file mode 100644 index 0000000..8444f0a --- /dev/null +++ b/2022/src/main.rs @@ -0,0 +1,75 @@ +use crate::solutions::*; +use aoc::read_file; +use std::env; +use std::fmt::Display; +use std::time::Instant; + +mod helpers; +mod solutions; + +static ANSI_ITALIC: &str = "\x1b[3m"; +static ANSI_BOLD: &str = "\x1b[1m"; +static ANSI_RESET: &str = "\x1b[0m"; + +fn print_result(func: impl FnOnce(&str) -> T, input: &str) { + let timer = Instant::now(); + let result = func(input); + let time = timer.elapsed(); + println!( + "{} {}(elapsed: {:.2?}){}", + result, ANSI_ITALIC, time, ANSI_RESET + ); +} + +macro_rules! solve_day { + ($day:path, $input:expr) => {{ + use $day::*; + println!("----"); + println!(""); + println!("πŸŽ„ {}Part 1{} πŸŽ„", ANSI_BOLD, ANSI_RESET); + println!(""); + print_result(part_one, $input); + println!(""); + println!("πŸŽ„ {}Part 2{} πŸŽ„", ANSI_BOLD, ANSI_RESET); + println!(""); + print_result(part_two, $input); + println!(""); + println!("----"); + }}; +} + +fn main() { + let args: Vec = env::args().collect(); + let day: u8 = args[1].clone().parse().unwrap(); + let input = read_file("inputs", day); + + match day { + 1 => solve_day!(day01, &input), + 2 => solve_day!(day02, &input), + 3 => solve_day!(day03, &input), + 4 => solve_day!(day04, &input), + 5 => solve_day!(day05, &input), + 6 => solve_day!(day06, &input), + 7 => solve_day!(day07, &input), + 8 => solve_day!(day08, &input), + 9 => solve_day!(day09, &input), + 10 => solve_day!(day10, &input), + 11 => solve_day!(day11, &input), + 12 => solve_day!(day12, &input), + 13 => solve_day!(day13, &input), + 14 => solve_day!(day14, &input), + 15 => solve_day!(day15, &input), + 16 => solve_day!(day16, &input), + 17 => solve_day!(day17, &input), + 18 => solve_day!(day18, &input), + 19 => solve_day!(day19, &input), + 20 => solve_day!(day20, &input), + 21 => solve_day!(day21, &input), + 22 => solve_day!(day22, &input), + 23 => solve_day!(day23, &input), + 24 => solve_day!(day24, &input), + 25 => solve_day!(day25, &input), + 1 => solve_day!(day01, &input), + _ => println!("day not solved: {}", day), + } +} diff --git a/2022/src/solutions/.keep b/2022/src/solutions/.keep new file mode 100644 index 0000000..e69de29 diff --git a/2022/src/solutions/day01.rs b/2022/src/solutions/day01.rs new file mode 100644 index 0000000..f011ec9 --- /dev/null +++ b/2022/src/solutions/day01.rs @@ -0,0 +1,26 @@ +pub fn part_one(input: &str) -> u32 { + 0 +} + +pub fn part_two(input: &str) -> u32 { + 0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 1); + assert_eq!(part_one(&input), 0); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 1); + assert_eq!(part_two(&input), 0); + } +} diff --git a/2022/src/solutions/day02.rs b/2022/src/solutions/day02.rs new file mode 100644 index 0000000..29da7dd --- /dev/null +++ b/2022/src/solutions/day02.rs @@ -0,0 +1,76 @@ +struct Instruction<'a> { + direction: &'a str, + value: i32, +} + +struct Position { + x: i32, + y: i32, + aim: i32, +} + +fn to_instruction(line: &str) -> Instruction { + let (direction, _value) = line.split_once(' ').unwrap(); + + Instruction { + direction, + value: _value.parse().unwrap(), + } +} + +fn update_position(pos: Position, Instruction { direction, value }: Instruction) -> Position { + match direction { + "forward" => Position { + x: pos.x + value, + y: pos.y + pos.aim * value, + ..pos + }, + "down" => Position { + aim: pos.aim + value, + ..pos + }, + "up" => Position { + aim: pos.aim - value, + ..pos + }, + val => panic!("bad direction input: {}", val), + } +} + +pub fn part_one(input: &str) -> i32 { + let pos = input + .lines() + .map(to_instruction) + .fold(Position { x: 0, y: 0, aim: 0 }, update_position); + + // optimization: `aim` in part two mirrors `depth` in part one which allows us to reuse the positioning logic. + pos.x * pos.aim +} + +pub fn part_two(input: &str) -> i32 { + let pos = input + .lines() + .map(to_instruction) + .fold(Position { x: 0, y: 0, aim: 0 }, update_position); + + pos.x * pos.y +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 2); + assert_eq!(part_one(&input), 150); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 2); + assert_eq!(part_two(&input), 900); + } +} diff --git a/2022/src/solutions/day03.rs b/2022/src/solutions/day03.rs new file mode 100644 index 0000000..b3f6456 --- /dev/null +++ b/2022/src/solutions/day03.rs @@ -0,0 +1,89 @@ +use itertools::Itertools; +use std::collections::HashMap; + +pub fn arr_to_int(bits: &[bool]) -> u32 { + bits.iter().fold(0, |acc, &b| acc * 2 + (b as u32)) +} + +pub fn str_to_int(str: &str) -> u32 { + u32::from_str_radix(str, 2).unwrap() +} + +pub fn part_one(input: &str) -> u32 { + // counter that maps character indices to signed integers. + // we can later calculate the gamma value for a given index by checking the entry's sign. + let mut counter: HashMap = HashMap::new(); + + // for every character in a line: + // increment (1) or decrement (0) the counter entry for this index. + input.lines().for_each(|l| { + l.chars().enumerate().for_each(|(i, c)| { + let val = counter.entry(i).or_default(); + *val += if c == '1' { 1 } else { -1 }; + }) + }); + + // collect counter into a sorted byte array. + let gamma = counter + .keys() + .sorted_unstable() + .map(|i| *counter.get(i).unwrap() >= 0) + .collect_vec(); + + // derive epsilon by flipping each bit of gamma. + let epsilon = gamma.iter().map(|b| !(*b)).collect_vec(); + + arr_to_int(&gamma) * arr_to_int(&epsilon) +} + +pub fn part_two(input: &str) -> u32 { + let lines = input.lines().collect_vec(); + + let oxy_rating = + find_line_by_bit_criteria(|a, b| if a.len() >= b.len() { a } else { b }, &lines); + + let co2_rating = + find_line_by_bit_criteria(|a, b| if a.len() >= b.len() { b } else { a }, &lines); + + str_to_int(oxy_rating) * str_to_int(co2_rating) +} + +fn find_line_by_bit_criteria<'a>( + bit_criteria: impl Fn(Vec<&'a str>, Vec<&'a str>) -> Vec<&'a str>, + candidates: &[&'a str], +) -> &'a str { + let mut i = 0; + let mut survivors = candidates.to_vec(); + + while survivors.len() > 1 { + // partition lines by the dominant bit (1 or 0) in column `i`. + let (one_dominant, zero_dominant): (Vec<&str>, Vec<&str>) = survivors + .iter() + .partition(|s| s.chars().nth(i).unwrap() == '1'); + + // determine which group should be continued with and assign it as survivors, discarding the rest. + survivors = bit_criteria(one_dominant, zero_dominant); + i += 1; + } + + survivors.pop().unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 3); + assert_eq!(part_one(&input), 198); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 3); + assert_eq!(part_two(&input), 230); + } +} diff --git a/2022/src/solutions/day04.rs b/2022/src/solutions/day04.rs new file mode 100644 index 0000000..d45d4dc --- /dev/null +++ b/2022/src/solutions/day04.rs @@ -0,0 +1,143 @@ +use std::collections::{HashMap, HashSet}; + +static BOARD_SIZE: usize = 5; + +// boards are stored as a hash_map. this facilitates the removal of winning boards in the game loop. +struct Board { + // holds unique numbers on this board in a set. this makes summing uncrossed numbers easy and efficient. + nums: HashSet, + // holds both `horizontal` and `vertical` rows of ths board. + // optimization: pre-calculate a 'transposed' set of rows & columns once in the beginning. + // optimization: use signed integers for keys (- for column ids) to avoid expensive string operations. + rows: HashMap>, +} + +type Boards = HashMap; + +pub fn str_to_u32(s: &str) -> u32 { + s.trim().parse().unwrap() +} + +fn to_draw(line: &str) -> Vec { + line.split(',').map(str_to_u32).collect() +} + +fn to_board(lines: &[&str]) -> Board { + lines.iter().enumerate().fold( + Board { + nums: HashSet::new(), + rows: HashMap::new(), + }, + |mut board, (_row, l)| { + l.trim() + .split_whitespace() + .enumerate() + .for_each(|(_col, s)| { + let parsed: u32 = str_to_u32(s); + let col: i32 = _col as i32; + let row: i32 = _row as i32; + board.nums.insert(parsed); + board.rows.entry(-(col + 1)).or_default().push(parsed); + board.rows.entry(row + 1).or_default().push(parsed); + }); + + board + }, + ) +} + +fn to_boards<'a>(lines: impl Iterator) -> Boards { + let mut boards = HashMap::new(); + + lines + .filter(|l| !l.is_empty()) + .collect::>() + .chunks(BOARD_SIZE) + .map(to_board) + .enumerate() + .for_each(|(i, b)| { + boards.insert(i, b); + }); + + boards +} + +fn find_winners(draw: &[u32], boards: &Boards, i: usize) -> Vec<(usize, u32)> { + let current_draw = &draw[i - 1]; + let draws = &draw[..i]; + + boards + .iter() + .filter(|(_, b)| { + // optimization: skip board processing if it does not contain the drawn number. + b.nums.contains(current_draw) + // check if any row consists of crossed numbers only. + && b.rows.values().any(|v| v.iter().all(|n| draws.contains(n))) + }) + .map(|(key, b)| { + let uncrossed_nums: u32 = b.nums.iter().filter(|n| !draws.contains(n)).sum(); + (*key, current_draw * uncrossed_nums) + }) + .collect() +} + +fn find_first_winner(draw: &[u32], boards: &mut Boards, i: usize) -> u32 { + let winners = find_winners(draw, boards, i); + + if winners.is_empty() { + find_first_winner(draw, boards, i + 1) + } else { + let (_, score) = winners.first().unwrap(); + *score + } +} + +fn find_last_winner(draw: &[u32], boards: &mut Boards) -> u32 { + let mut winners: Vec = Vec::new(); + + for i in BOARD_SIZE..draw.len() { + find_winners(draw, boards, i) + .iter() + .for_each(|(key, score)| { + boards.remove(key); + winners.push(*score); + }); + } + + *winners.last().unwrap() +} + +pub fn part_one(input: &str) -> u32 { + let mut lines = input.lines(); + let draw: Vec = to_draw(lines.next().unwrap()); + let mut boards = to_boards(lines); + + find_first_winner(&draw, &mut boards, BOARD_SIZE) +} + +pub fn part_two(input: &str) -> u32 { + let mut lines = input.lines(); + let draw: Vec = to_draw(lines.next().unwrap()); + let mut boards = to_boards(lines); + + find_last_winner(&draw, &mut boards) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 4); + assert_eq!(part_one(&input), 4512); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 4); + assert_eq!(part_two(&input), 1924); + } +} diff --git a/2022/src/solutions/day05.rs b/2022/src/solutions/day05.rs new file mode 100644 index 0000000..3571039 --- /dev/null +++ b/2022/src/solutions/day05.rs @@ -0,0 +1,92 @@ +use std::{collections::HashMap, convert::TryInto}; + +struct Point { + x: i32, + y: i32, +} + +type Line = (Point, Point); +type Grid = HashMap<(i32, i32), u32>; + +trait PointGrid { + fn add_point(&mut self, x: i32, y: i32); + fn add_points(&mut self, line: &str, skip_diagonals: bool); + fn overlaps(&self) -> u32; +} + +impl PointGrid for Grid { + fn add_point(&mut self, x: i32, y: i32) { + *self.entry((x, y)).or_default() += 1; + } + + fn add_points(&mut self, line: &str, skip_diagonals: bool) { + let (p1, p2) = parse_line(line); + + if skip_diagonals && (p1.x != p2.x && p1.y != p2.y) { + return; + } + + let mut x = p1.x; + let mut y = p1.y; + + let dx = (p2.x - p1.x).signum(); + let dy = (p2.y - p1.y).signum(); + + while (x, y) != (p2.x + dx, p2.y + dy) { + *self.entry((x, y)).or_default() += 1; + x += dx; + y += dy; + } + } + + fn overlaps(&self) -> u32 { + self.values() + .filter(|v| **v > 1) + .count() + .try_into() + .unwrap() + } +} + +fn parse_line(l: &str) -> Line { + let mut parts = l.split(" -> ").map(|p| { + let mut nums = p.split(',').map(|x| x.parse().unwrap()); + Point { + x: nums.next().unwrap(), + y: nums.next().unwrap(), + } + }); + + (parts.next().unwrap(), parts.next().unwrap()) +} + +pub fn part_one(input: &str) -> u32 { + let mut grid: Grid = HashMap::new(); + input.lines().for_each(|l| grid.add_points(l, true)); + grid.overlaps() +} + +pub fn part_two(input: &str) -> u32 { + let mut grid: Grid = HashMap::new(); + input.lines().for_each(|l| grid.add_points(l, false)); + grid.overlaps() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 5); + assert_eq!(part_one(&input), 5); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 5); + assert_eq!(part_two(&input), 12); + } +} diff --git a/2022/src/solutions/day06.rs b/2022/src/solutions/day06.rs new file mode 100644 index 0000000..b3a3a0d --- /dev/null +++ b/2022/src/solutions/day06.rs @@ -0,0 +1,81 @@ +static REPRO_INTERVAL_INITIAL: usize = 9; +static REPRO_INTERVAL: usize = 7; + +fn get_og_fishes(input: &str) -> Vec { + input + .split(',') + .map(|s| s.trim().parse().unwrap()) + .collect() +} + +fn project_population(members: Vec, generations: &mut [usize]) -> usize { + let interval_count = generations.len(); + let mut pop = members.len(); + + // for each fish in the OG generation, spawn an offspring at {rest_timer}. + // we have to project their adult life as well since OGs continue reproducing. + members.iter().for_each(|f| { + generations[*f] += 1; + project_adult_generation(generations, 1, *f, interval_count); + }); + + // a generation of fishes hatches every day. + // we can project them as a whole since there is no variance in repro. rate in a generation. + for i in 0..interval_count { + let generation_size = generations[i]; + // add hatched fishes to the population counter. + pop += generation_size; + + // initial reproduction is delayed for hatched fishes. we handle it with a special case. + let adults_at = i + REPRO_INTERVAL_INITIAL; + + if adults_at < interval_count { + generations[adults_at] += generation_size; + // Once fishes are adults, we can project the rest of {interval_count} in a loop. + project_adult_generation(generations, generation_size, adults_at, interval_count); + } + } + + pop +} + +fn project_adult_generation( + generations: &mut [usize], + generation_size: usize, + current_interval: usize, + interval_count: usize, +) { + let mut spawns_at = current_interval + REPRO_INTERVAL; + // increase the generation_size at {interval} by the current generation_size for rest of observed interval. + while spawns_at < interval_count { + generations[spawns_at] += generation_size; + spawns_at += REPRO_INTERVAL; + } +} + +pub fn part_one(input: &str) -> usize { + project_population(get_og_fishes(input), &mut [0; 80]) +} + +pub fn part_two(input: &str) -> usize { + project_population(get_og_fishes(input), &mut [0; 256]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 6); + assert_eq!(part_one(&input), 5934); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 6); + assert_eq!(part_two(&input), 26984457539); + } +} diff --git a/2022/src/solutions/day07.rs b/2022/src/solutions/day07.rs new file mode 100644 index 0000000..b6e728b --- /dev/null +++ b/2022/src/solutions/day07.rs @@ -0,0 +1,54 @@ +use crate::helpers::math::{median, nth_triangular}; + +fn parse(input: &str) -> Vec { + input + .lines() + .next() + .unwrap() + .split(',') + .map(|x| x.parse().unwrap()) + .collect() +} + +pub fn part_one(input: &str) -> u64 { + let mut positions = parse(input); + let median = median(&mut positions); + positions + .iter() + .map(|x| (*x as i32 - median as i32).abs() as u64) + .sum() +} + +pub fn part_two(input: &str) -> u64 { + let mut positions = parse(input); + positions.sort_unstable(); + + (0..*positions.last().unwrap()) + .map(|i| { + positions + .iter() + .map(|p| nth_triangular((*p as i32 - i as i32).abs() as u64)) + .sum() + }) + .min() + .unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 7); + assert_eq!(part_one(&input), 37); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 7); + assert_eq!(part_two(&input), 168); + } +} diff --git a/2022/src/solutions/day08.rs b/2022/src/solutions/day08.rs new file mode 100644 index 0000000..3ccb63f --- /dev/null +++ b/2022/src/solutions/day08.rs @@ -0,0 +1,185 @@ +use std::collections::HashMap; + +use itertools::Itertools; + +pub fn part_one(input: &str) -> usize { + input + .lines() + .flat_map(|l| { + l.split(" | ").last().unwrap().split(' ').filter(|s| { + let len = s.len(); + (2..=4).contains(&len) || len == 7 + }) + }) + .count() +} + +type Pattern = Vec; + +/// The display is made up of segments `a-g` mapped as follows: +/// aaaa +/// b c +/// b c +/// dddd +/// e f +/// e f +/// gggg +type Display = HashMap; + +trait DisplayMethods { + fn decode(&self, pattern: &[char]) -> char; +} + +impl DisplayMethods for Display { + /// Once a display is fully reconstructed, we can decode digits with it. + /// Individual digits are returned as strings since we need to join 4 digits in the caller. + fn decode(&self, pattern: &[char]) -> char { + if is_one(pattern) { + '1' + } else if is_four(pattern) { + '4' + } else if is_seven(pattern) { + '7' + } else if is_eight(pattern) { + '8' + } else { + let displayed: String = pattern + .iter() + .map(|c| self.get(c).unwrap()) + .sorted_unstable() + .collect(); + + match displayed.as_ref() { + "abcefg" => '0', + "acdeg" => '2', + "acdfg" => '3', + "abdfg" => '5', + "abdefg" => '6', + "abcdfg" => '9', + val => panic!("unexpected decoded pattern: {}", val), + } + } + } +} + +// c,f +fn is_one(pattern: &[char]) -> bool { + pattern.len() == 2 +} + +// b,c,d,f +fn is_four(pattern: &[char]) -> bool { + pattern.len() == 4 +} + +// a,c,f +fn is_seven(pattern: &[char]) -> bool { + pattern.len() == 3 +} + +// a,b,c,d,e,f,g +fn is_eight(pattern: &[char]) -> bool { + pattern.len() == 7 +} + +fn is_six(p: &[char], one: &[char]) -> bool { + one.iter().any(|c| !p.contains(c)) +} + +fn is_zero(p: &[char], four: &[char]) -> bool { + four.iter().any(|c| !p.contains(c)) +} + +/// Helper trait to make `.find()`ing the first digits less verbose. +fn find_by(signal: &[Vec], find_fn: impl Fn(&[char]) -> bool) -> Pattern { + signal.iter().find(|x| find_fn(x)).unwrap().to_owned() +} + +pub fn part_two(input: &str) -> u32 { + input + .lines() + .map(|l| { + let patterns: Vec = l + .replace(" |", "") + .split(' ') + .map(|s| s.chars().collect()) + .collect(); + + let signal = patterns[0..10].to_vec(); + let outputs = patterns[10..14].to_vec(); + + let mut display = Display::new(); + + let one = find_by(&signal, is_one); + let seven = find_by(&signal, is_seven); + let four = find_by(&signal, is_four); + let eight = find_by(&signal, is_eight); + + // once we know `c` and `f`, we can isolate `a` by looking at `7` + display.insert(*seven.iter().find(|c| !&one.contains(c)).unwrap(), 'a'); + + // at this point, we can decode the full signal by looking at six-segment components. + for p in signal.iter().filter(|x| x.len() == 6) { + if is_six(p, &one) { + for c in &one { + if p.contains(c) { + display.insert(*c, 'f'); + } else { + display.insert(*c, 'c'); + } + } + } else if is_zero(p, &four) { + for c in &four { + if !p.contains(c) { + display.insert(*c, 'd'); + } else if !&one.contains(c) { + display.insert(*c, 'b'); + } + } + } else { + for c in &eight { + if !p.contains(c) { + display.insert(*c, 'e'); + } + } + } + } + + // whatever segment is left over maps to the last needed segment `g`. + // we can use `eight` to identify it since it has all segments. + for c in &eight { + if !(display.contains_key(c)) { + display.insert(*c, 'g'); + } + } + + // the display is ready for decoding now. + // We decode the 4-digit number to a string and then parse it to an int. + let num = outputs.iter().fold(String::new(), |mut acc, p| { + acc.push(display.decode(p)); + acc + }); + + num.parse::().unwrap() + }) + .sum() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 8); + assert_eq!(part_one(&input), 26); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 8); + assert_eq!(part_two(&input), 61229); + } +} diff --git a/2022/src/solutions/day09.rs b/2022/src/solutions/day09.rs new file mode 100644 index 0000000..ab299af --- /dev/null +++ b/2022/src/solutions/day09.rs @@ -0,0 +1,131 @@ +type Matrix = Vec>; + +#[derive(Clone, Copy, PartialEq)] +struct Point { + x: usize, + y: usize, + val: u32, +} + +fn parse(input: &str) -> Matrix { + input + .lines() + .map(|l| { + l.chars() + .map(|c| c.to_digit(10).unwrap()) + .collect::>() + }) + .collect() +} + +fn surounding_points(matrix: &[Vec], p: &Point) -> [Point; 4] { + let line = &matrix[p.y]; + // using an array istd. of a vec here achieves a 5x speedup by utilising the stack. + // in this case, we need to return "bogus" values for points that would otherwise result in `-1`. + // convention: we use index `99` and set value to `9` to mark it as an edge. + [ + Point { + x: if p.x > 0 { p.x - 1 } else { 99 }, + y: p.y, + val: if p.x > 0 { line[p.x - 1] } else { 9 }, + }, + Point { + x: p.x + 1, + y: p.y, + val: if p.x < line.len() - 1 { + line[p.x + 1] + } else { + 9 + }, + }, + Point { + x: p.x, + y: if p.y > 0 { p.y - 1 } else { 99 }, + val: if p.y > 0 { matrix[p.y - 1][p.x] } else { 9 }, + }, + Point { + x: p.x, + y: p.y + 1, + val: if p.y < matrix.len() - 1 { + matrix[p.y + 1][p.x] + } else { + 9 + }, + }, + ] +} + +fn is_minimum(matrix: &[Vec], p: &Point) -> bool { + surounding_points(matrix, p).iter().all(|x| x.val > p.val) +} + +fn get_minimums(matrix: &[Vec]) -> Vec { + let mut minimums: Vec = Vec::new(); + + for y in 0..matrix.len() { + for x in 0..matrix[0].len() { + let val = matrix[y][x]; + let p = Point { x, y, val }; + if is_minimum(matrix, &p) { + minimums.push(p); + } + } + } + + minimums +} + +pub fn part_one(input: &str) -> u32 { + let matrix = parse(input); + get_minimums(&matrix).iter().map(|p| p.val + 1).sum() +} + +fn flood_fill<'a>(matrix: &'a [Vec], p: &'a Point, basin: &'a mut Vec) { + surounding_points(matrix, p) + .iter() + .filter(|x| x.val != 9 && x.val > p.val) + .for_each(|x| { + flood_fill(matrix, x, basin); + }); + + if !basin.contains(p) { + basin.push(*p); + } +} + +pub fn part_two(input: &str) -> usize { + let matrix = parse(input); + + let mut basins = get_minimums(&matrix) + .iter() + .map(|p| { + let mut basin: Vec = Vec::new(); + flood_fill(&matrix, p, &mut basin); + basin.len() + }) + .collect::>(); + + let len = basins.len(); + basins.sort_unstable(); + + basins[len - 1] * basins[len - 2] * basins[len - 3] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 9); + assert_eq!(part_one(&input), 15); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 9); + assert_eq!(part_two(&input), 1134); + } +} diff --git a/2022/src/solutions/day10.rs b/2022/src/solutions/day10.rs new file mode 100644 index 0000000..4f6a457 --- /dev/null +++ b/2022/src/solutions/day10.rs @@ -0,0 +1,119 @@ +use crate::helpers::math::median; + +/// tracks open tokens (e.g. `(`) in sequence of occurence. +type CharacterStack = Vec; + +/// error thrown if parser fails to parse a line. +struct ParsingError { + token: char, +} + +type ParsingResult = Result; + +fn opener(c: char) -> Option { + match c { + ')' => Some('('), + ']' => Some('['), + '}' => Some('{'), + '>' => Some('<'), + _ => None, + } +} + +/// go through the line char-by-char. +/// opening chars are added to a stack. +/// when closing char is encountered, pop the first item of the stack. +/// if the closing char can be used to close the pair, continue processing the line. +/// if it does not match, throw a `ParsingError` referencing the offending token. +/// once the line completes parsing without errors, return the rest of the stack. +fn parse(line: &str) -> ParsingResult { + let mut stack: CharacterStack = Vec::new(); + let mut offending_token: Option = None; + + for c in line.chars() { + let opener = opener(c); + + if let Some(opener) = opener { + match stack.pop() { + Some(last_open) => { + if opener != last_open { + offending_token = Some(c); + break; + } + } + // case doesn't seem to occur in puzzle input. + None => { + offending_token = Some(c); + break; + } + } + } else { + stack.push(c); + } + } + + match offending_token { + Some(token) => Err(ParsingError { token }), + None => Ok(stack), + } +} + +pub fn part_one(input: &str) -> u32 { + input + .lines() + .map(|l| match parse(l) { + Ok(_) => 0, + Err(err) => match err.token { + ')' => 3, + ']' => 57, + '}' => 1197, + '>' => 25137, + _ => 0, + }, + }) + .sum() +} + +pub fn part_two(input: &str) -> u64 { + let mut scores: Vec = input + .lines() + .filter_map(|l| match parse(l) { + Ok(stack) => { + let score = stack.iter().rev().fold(0, |acc, char| { + acc * 5 + + match char { + '(' => 1, + '[' => 2, + '{' => 3, + '<' => 4, + _ => 0, + } + }); + + Some(score) + } + Err(_) => None, + }) + .collect(); + + median(&mut scores) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 10); + assert_eq!(part_one(&input), 26397); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 10); + assert_eq!(part_two(&input), 288957); + } +} diff --git a/2022/src/solutions/day11.rs b/2022/src/solutions/day11.rs new file mode 100644 index 0000000..c34d5a7 --- /dev/null +++ b/2022/src/solutions/day11.rs @@ -0,0 +1,122 @@ +use crate::helpers::grid::Point; +use std::collections::HashSet; + +static OCTOPUS_ROWS: usize = 10; +static OCTOPUS_COLS: usize = 10; + +type Grid = [Line; 10]; +type Line = [u32; 10]; + +fn parse(input: &str) -> Grid { + input + .lines() + .map(|l| { + l.chars() + .map(|c| c.to_digit(10).unwrap()) + .collect::>() + .try_into() + .unwrap() + }) + .collect::>() + .try_into() + .unwrap() +} + +fn process_step(grid: &mut Grid, all_points: &[Point]) -> u32 { + let mut flashed: HashSet = HashSet::new(); + // start the flash cascade by incrementing all points in the grid. + // `tick` calls itself recursively until either all octopus have flashed or there + // is a `tick` where no octopus reaches required energy levels. + tick(grid, &mut flashed, all_points); + // reset all flashed octopus to `0`. + reset_energy_levels(grid, all_points); + // once there is no more processing to do for a step, we return the count of flashes observed. + flashed.len() as u32 +} + +fn tick(grid: &mut Grid, flashed: &mut HashSet, points: &[Point]) { + points.iter().for_each(|Point(x, y)| { + grid[*y][*x] += 1; + if grid[*y][*x] > 9 && !flashed.contains(&Point(*x, *y)) { + let p = Point(*x, *y); + flashed.insert(p); + tick( + grid, + flashed, + // when an octopus flashes, it increments all neighbors. + &p.neighbors(OCTOPUS_COLS - 1, OCTOPUS_ROWS - 1, true), + ); + } + }); +} + +fn reset_energy_levels(grid: &mut Grid, points: &[Point]) { + points.iter().for_each(|Point(x, y)| { + if grid[*y][*x] > 9 { + grid[*y][*x] = 0; + } + }); +} + +fn all_points() -> Vec { + let mut points = Vec::new(); + + for x in 0..OCTOPUS_COLS { + for y in 0..OCTOPUS_ROWS { + points.push(Point(x, y)); + } + } + + points +} + +pub fn part_one(input: &str) -> u32 { + let mut grid = parse(input); + // optimization: keep a reference of all points in the grid to avoid recomputing this constantly. + let points = all_points(); + + let mut flash_count: u32 = 0; + for _ in 0..100 { + flash_count += process_step(&mut grid, &points); + } + + flash_count +} + +pub fn part_two(input: &str) -> usize { + let mut grid = parse(input); + // optimization: keep a reference of all points in the grid to avoid recomputing this constantly. + let points = all_points(); + + let mut index: usize = 0; + let mut all_flashed = false; + + while !all_flashed { + if process_step(&mut grid, &points) == (OCTOPUS_COLS * OCTOPUS_ROWS) as u32 { + all_flashed = true + } else { + index += 1; + } + } + + index + 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 11); + assert_eq!(part_one(&input), 1656); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 11); + assert_eq!(part_two(&input), 195); + } +} diff --git a/2022/src/solutions/day12.rs b/2022/src/solutions/day12.rs new file mode 100644 index 0000000..9c821f5 --- /dev/null +++ b/2022/src/solutions/day12.rs @@ -0,0 +1,109 @@ +use std::collections::{HashMap, HashSet}; + +static START: &str = "start"; +static END: &str = "end"; + +#[derive(Debug)] +struct Graph<'a> { + // nodes are stored as map where size is a boolean. + // `true` indicates that a room is big, `false` small. + nodes: HashMap<&'a str, bool>, + // edges are stored as an adjacency list for each node. + edges: HashMap<&'a str, HashSet<&'a str>>, +} + +impl Graph<'_> { + fn new() -> Self { + Graph { + nodes: HashMap::new(), + edges: HashMap::new(), + } + } + + fn get_adjacent_nodes(&self, node_id: &str) -> Vec<&&str> { + self.edges.get(node_id).unwrap().iter().collect() + } +} + +fn parse(input: &str) -> Graph { + let mut graph = Graph::new(); + + input.lines().for_each(|l| { + let mut node_pair = l.split('-').map(|id| (id, id.to_uppercase() == id)); + + let (from, from_type) = node_pair.next().unwrap(); + let (to, to_type) = node_pair.next().unwrap(); + + graph.nodes.insert(from, from_type); + graph.nodes.insert(to, to_type); + graph.edges.entry(from).or_default().insert(to); + graph.edges.entry(to).or_default().insert(from); + }); + + graph +} + +fn search(graph: &Graph, seen: &HashSet<&str>, id: &str, small_room_counter: u8) -> u32 { + let mut small_room_counter = small_room_counter; + + if id == END { + return 1; + } + + if seen.contains(&id) { + if id == START { + return 0; + // in part one, any small room can only be visited once. + // in part two, **one** room may be visited twice. + // to reflect that, a counter is decremented the first time a small room is encountered. + } else if !*graph.nodes.get(id).unwrap() { + if small_room_counter == 0 { + return 0; + } else { + small_room_counter -= 1; + } + } + } + + let mut seen_here = seen.clone(); + seen_here.insert(id); + + let result = graph + .get_adjacent_nodes(id) + .iter() + .map(|adjacent_node| search(graph, &seen_here, adjacent_node, small_room_counter)) + .sum(); + + result +} + +pub fn part_one(input: &str) -> u32 { + let graph = parse(input); + let seen: HashSet<&str> = HashSet::new(); + search(&graph, &seen, START, 0) +} + +pub fn part_two(input: &str) -> u32 { + let graph = parse(input); + let seen: HashSet<&str> = HashSet::new(); + search(&graph, &seen, START, 1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 12); + assert_eq!(part_one(&input), 226); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 12); + assert_eq!(part_two(&input), 3509); + } +} diff --git a/2022/src/solutions/day13.rs b/2022/src/solutions/day13.rs new file mode 100644 index 0000000..3623652 --- /dev/null +++ b/2022/src/solutions/day13.rs @@ -0,0 +1,157 @@ +#![allow(clippy::needless_range_loop)] +use crate::helpers::grid::Point; +use std::cmp::max; + +type Points = Vec; + +type Line = Vec; +type Grid = Vec; + +#[derive(Debug)] +enum Instruction { + X(usize), + Y(usize), +} +type Instructions = Vec; + +fn parse(input: &str) -> (Grid, Instructions) { + let mut points: Points = Vec::new(); + let mut instructions: Instructions = Vec::new(); + + let mut width: usize = 0; + let mut height: usize = 0; + + input.lines().for_each(|l| { + // line is an instruction. + if l.starts_with('f') { + let instruction = parse_instruction(l); + + // infer grid size from first instructions. + // looking at max. point size might fail if last lines or columns are empty. + if width == 0 || height == 0 { + match instruction { + Instruction::X(x) => width = max(x * 2 + 1, width), + Instruction::Y(y) => height = max(y * 2 + 1, height), + } + } + + instructions.push(instruction); + // line is a point. + } else if !l.is_empty() { + points.push(parse_point(l)); + } + }); + + (make_grid(&points, width, height), instructions) +} + +fn parse_point(l: &str) -> Point { + let mut coords = l.split(','); + let x: usize = coords.next().unwrap().parse().unwrap(); + let y: usize = coords.next().unwrap().parse().unwrap(); + Point(x, y) +} + +fn parse_instruction(l: &str) -> Instruction { + let mut instr = l.split(' ').last().unwrap().split('='); + let axis = instr.next().unwrap(); + let amount: usize = instr.next().unwrap().parse().unwrap(); + + if axis == "x" { + Instruction::X(amount) + } else { + Instruction::Y(amount) + } +} + +fn make_grid(points: &[Point], width: usize, height: usize) -> Grid { + let mut grid: Grid = vec![vec![false; width]; height]; + + for Point(x, y) in points { + grid[*y][*x] = true; + } + + grid +} + +fn fold_y(grid: &[Line], fold_at: usize, width: usize, height: usize) -> Grid { + let mut points: Points = Vec::new(); + + for y in 0..fold_at { + for x in 0..width { + if grid[y][x] || grid[height - y - 1][x] { + points.push(Point(x, y)); + } + } + } + + make_grid(&points, width, fold_at) +} + +fn fold_x(grid: &[Line], fold_at: usize, width: usize, height: usize) -> Grid { + let mut points: Points = Vec::new(); + + for y in 0..height { + for x in 0..fold_at { + if grid[y][x] || grid[y][width - x - 1] { + points.push(Point(x, y)); + } + } + } + + make_grid(&points, fold_at, height) +} + +fn fold(grid: &[Line], instruction: &Instruction) -> Grid { + let height = grid.len(); + let width = grid[0].len(); + + match instruction { + Instruction::X(fold_at) => fold_x(grid, *fold_at, width, height), + Instruction::Y(fold_at) => fold_y(grid, *fold_at, width, height), + } +} + +fn count_grid(grid: &[Line]) -> u32 { + grid.iter().flatten().filter(|x| **x).count() as u32 +} + +pub fn part_one(input: &str) -> u32 { + let (grid, instructions) = parse(input); + count_grid(&fold(&grid, &instructions[0])) +} + +fn print_grid(grid: &[Line]) { + for line in grid { + let chars: String = line.iter().map(|x| if *x { '#' } else { '.' }).collect(); + println!("{}", chars); + } +} + +pub fn part_two(input: &str) -> u32 { + let (grid, instructions) = parse(input); + + let code = instructions.iter().fold(grid, |acc, curr| fold(&acc, curr)); + + print_grid(&code); + count_grid(&code) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 13); + assert_eq!(part_one(&input), 17); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 13); + assert_eq!(part_two(&input), 16); + } +} diff --git a/2022/src/solutions/day14.rs b/2022/src/solutions/day14.rs new file mode 100644 index 0000000..c0b2759 --- /dev/null +++ b/2022/src/solutions/day14.rs @@ -0,0 +1,116 @@ +use itertools::{Itertools, MinMaxResult}; +use std::collections::HashMap; + +type Pair = (char, char); +type Rules = HashMap; + +/// Compact representation of a polymer string. This is a necessity to solve part 2 where my naive first approach failed. +/// `pairs` counts the # of times a character combination is present in the polymer. +/// example: `{ (N, N): 1, (N, C): 1, (C, B): 1 }` for the example polymer. +/// A polymerization step creates a new set of pairs by processing `rules` for every pair currently in the polymer. +/// example: `{ (N, N): 3 }` produces `{ (N, C): 2, (C, N): 2 }`. +/// Looking at `pairs` is not enough to derive character counts, so we need a `chars` map to count these as well. +/// whenever a character is added to a pair, we increment a running counter for that character. +/// example: `{ (N, N): 3 }` would increment `C` by 2. +#[derive(Clone)] +struct Polymer { + pairs: HashMap, + characters: HashMap, +} + +impl Polymer { + fn new() -> Self { + Polymer { + pairs: HashMap::new(), + characters: HashMap::new(), + } + } + + fn from_string(line: &str) -> Self { + let mut polymer = Polymer::new(); + let chars = line.chars(); + + chars.clone().for_each(|c| { + *polymer.characters.entry(c).or_default() += 1; + }); + + chars.clone().tuple_windows().for_each(|(a, b)| { + *polymer.pairs.entry((a, b)).or_default() += 1; + }); + + polymer + } + + fn expand(&mut self, rules: &Rules) { + let pairs = self.pairs.clone(); + self.pairs.clear(); + + for (pair, count) in pairs { + let to_add = rules.get(&pair).unwrap().to_owned(); + *self.pairs.entry((pair.0, to_add)).or_default() += count; + *self.pairs.entry((to_add, pair.1)).or_default() += count; + *self.characters.entry(to_add).or_default() += count; + } + } + + fn expand_times(&mut self, times: u8, rules: &Rules) -> &Self { + for _ in 0..times { + self.expand(rules); + } + self + } + + fn delta(&self) -> u64 { + match self.characters.values().minmax() { + MinMaxResult::MinMax(a, b) => b - a, + _ => unreachable!(), + } + } +} + +fn parse(input: &str) -> (Polymer, Rules) { + let mut lines = input.lines(); + + let polymer = Polymer::from_string(lines.next().unwrap()); + + let rules = lines.fold(HashMap::new(), |mut acc, l| { + if !l.is_empty() { + let parts: Vec = l.replace(" -> ", "").chars().collect(); + let pair = (parts[0], parts[1]); + acc.insert(pair, parts[2]); + } + + acc + }); + + (polymer, rules) +} + +pub fn part_one(input: &str) -> u64 { + let (mut polymer, rules) = parse(input); + polymer.expand_times(10, &rules).delta() +} + +pub fn part_two(input: &str) -> u64 { + let (mut polymer, rules) = parse(input); + polymer.expand_times(40, &rules).delta() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 14); + assert_eq!(part_one(&input), 1588); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 14); + assert_eq!(part_two(&input), 2188189693529); + } +} diff --git a/2022/src/solutions/day15.rs b/2022/src/solutions/day15.rs new file mode 100644 index 0000000..6d2fbd0 --- /dev/null +++ b/2022/src/solutions/day15.rs @@ -0,0 +1,148 @@ +/// implementation of Dijkstra's algorithm for a 2d grid. +/// borrows from the example found in the [rust docs](https://doc.rust-lang.org/std/collections/binary_heap/index.html#examples). +/// in contrast to the example, we do not create a directed graph but work with the supplied grid directly. +/// for further information, see: +/// [Wikipedia](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) | +/// [Introduction to the A* Algorithm](https://www.redblobgames.com/pathfinding/a-star/introduction.html). +mod shortest_path { + use crate::helpers::grid::Point; + use std::cmp::Ordering; + use std::collections::BinaryHeap; + + // while performing the search, track a sorted list of candidates (=state) to visit next on a priority queue. + #[derive(Copy, Clone, Eq, PartialEq)] + struct State { + cost: usize, + position: usize, + } + + /// the algorithm expects a `min-heap` priority queue as frontier. + /// the default std. lib implementation is a `max-heap`, so the sort order needs to be flipped for state values. + /// also adds a tie breaker based on position. see [rust docs](https://doc.rust-lang.org/std/collections/struct.BinaryHeap.html#min-heap) + impl Ord for State { + fn cmp(&self, other: &Self) -> Ordering { + other + .cost + .cmp(&self.cost) + .then_with(|| self.position.cmp(&other.position)) + } + } + + impl PartialOrd for State { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + pub fn shortest_path(grid: &[Vec]) -> Option { + let height = grid.len(); + let width = grid[0].len(); + + // dist[node] = current shortest distance from `start` to `node`. + let mut dist: Vec<_> = (0..(height * width)).map(|_| usize::MAX).collect(); + + let mut frontier = BinaryHeap::new(); + + let start_node = Point(0, 0).to_id(width); + let target_node = Point(height - 1, width - 1).to_id(width); + + // initialize start with a zero cost. + dist[start_node] = 0; + frontier.push(State { + cost: 0, + position: start_node, + }); + + // examine the frontier starting with the lowest cost nodes. + while let Some(State { cost, position }) = frontier.pop() { + if position == target_node { + return Some(cost); + } + + // skip: there is a better path to this node already. + if cost > dist[position] { + continue; + } + + // see if we can find a path with a lower cost than previous paths for any adjacent nodes. + for point in Point::from_id(position, width).neighbors(width - 1, height - 1, false) { + let next = State { + cost: cost + grid[point.1][point.0] as usize, + position: point.to_id(width), + }; + + // if so, add it to the frontier and continue. + if next.cost < dist[next.position] { + frontier.push(next); + dist[next.position] = next.cost; + } + } + } + + None + } +} + +use self::shortest_path::shortest_path; + +type Row = Vec; +type Grid = Vec; + +fn parse(input: &str) -> Grid { + input + .lines() + .map(|l| l.chars().map(|c| c.to_digit(10).unwrap()).collect()) + .collect() +} + +pub fn part_one(input: &str) -> u32 { + shortest_path(&parse(input)).unwrap() as u32 +} + +pub fn part_two(input: &str) -> u32 { + let grid = parse(input); + + let height = grid.len(); + let width = grid[0].len(); + + let expanded: Grid = (0..(5 * grid.len())) + .map(|y| { + (0..(5 * grid[0].len())) + .map(|x| { + // increment grows by one with every horizontal *and* vertical tile. + let x_increment = (x / width) as u32; + let y_increment = (y / height) as u32; + + // each individual value can be derived from the original value and the current distance to it. + let cost = grid[x % width][y % height] + x_increment + y_increment; + if cost == 9 { + cost + } else { + cost % 9 + } + }) + .collect() + }) + .collect(); + + shortest_path(&expanded).unwrap() as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 15); + assert_eq!(part_one(&input), 40); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 15); + assert_eq!(part_two(&input), 315); + } +} diff --git a/2022/src/solutions/day16.rs b/2022/src/solutions/day16.rs new file mode 100644 index 0000000..2608a0a --- /dev/null +++ b/2022/src/solutions/day16.rs @@ -0,0 +1,214 @@ +mod decoder { + #[derive(Clone, Debug)] + pub enum Packet { + Operator(Operator), + Literal(Literal), + } + + #[derive(Clone, Debug)] + pub struct Literal { + pub header: Header, + pub value: u64, + size: usize, + } + + #[derive(Clone, Debug)] + pub struct Operator { + pub header: Header, + pub children: Vec, + size: usize, + } + + #[derive(Clone, Debug)] + pub struct Header { + pub version: u64, + pub type_id: u64, + } + + pub fn decode(message: &str) -> Packet { + decode_packet(&decode_message(message)) + } + + fn decode_packet(bits: &[u8]) -> Packet { + if to_u64(&bits[3..6]) == 4 { + decode_literal(bits) + } else { + decode_operator(bits) + } + } + + fn decode_literal(bits: &[u8]) -> Packet { + let rest = &bits[6..]; + let mut index = 0; + let mut num = Vec::new(); + + while index <= rest.len() - 5 { + let next = index + 5; + let chunk = &rest[index..next]; + let signal = chunk[0]; + + num.extend(&chunk[1..5]); + index = next; + + if signal == 0 { + break; + } + } + + Packet::Literal(Literal { + header: decode_header(bits), + value: to_u64(&num), + size: 6 + index, + }) + } + + fn decode_operator(bits: &[u8]) -> Packet { + let mode = bits[6]; + let content_offset = 7 + if mode == 0 { 15 } else { 11 }; + let len = to_u64(&bits[7..content_offset]) as usize; + + let mut children = Vec::new(); + let mut index = 0; + + while (mode == 0 && index < len) || (mode == 1 && children.len() < len) { + let packet = decode_packet(&bits[(content_offset + index)..]); + + match &packet { + Packet::Literal(data) => index += data.size, + Packet::Operator(data) => index += data.size, + } + + children.push(packet); + } + + Packet::Operator(Operator { + header: decode_header(bits), + children, + size: content_offset + index, + }) + } + + fn decode_header(bits: &[u8]) -> Header { + Header { + version: to_u64(&bits[0..3]), + type_id: to_u64(&bits[3..6]), + } + } + + // instruction set is small, use a lookup table. + fn decode_message(message: &str) -> Vec { + message + .chars() + .flat_map(|c| match c { + '0' => [0, 0, 0, 0], + '1' => [0, 0, 0, 1], + '2' => [0, 0, 1, 0], + '3' => [0, 0, 1, 1], + '4' => [0, 1, 0, 0], + '5' => [0, 1, 0, 1], + '6' => [0, 1, 1, 0], + '7' => [0, 1, 1, 1], + '8' => [1, 0, 0, 0], + '9' => [1, 0, 0, 1], + 'A' => [1, 0, 1, 0], + 'B' => [1, 0, 1, 1], + 'C' => [1, 1, 0, 0], + 'D' => [1, 1, 0, 1], + 'E' => [1, 1, 1, 0], + 'F' => [1, 1, 1, 1], + c => panic!("unexpected token in message: {}", c), + }) + .collect() + } + + fn to_u64(bits: &[u8]) -> u64 { + bits.iter().fold(0, |acc, &b| acc * 2 + (b as u64)) + } +} + +mod interpreter { + use super::decoder::Packet; + + pub fn interpret(packet: Packet) -> u64 { + match packet { + Packet::Literal(_) => panic!("unexpected literal on root level."), + Packet::Operator(data) => { + let values: Vec = data + .children + .iter() + .map(|child| match child { + Packet::Literal(child_data) => child_data.value as u64, + Packet::Operator(child_data) => { + interpret(Packet::Operator(child_data.clone())) + } + }) + .collect(); + + match data.header.type_id { + 0 => values.iter().sum(), + 1 => values.iter().product(), + 2 => *values.iter().min().unwrap(), + 3 => *values.iter().max().unwrap(), + 5 => (values[0] > values[1]) as u64, + 6 => (values[0] < values[1]) as u64, + 7 => (values[0] == values[1]) as u64, + c => panic!("unknown type_id {}", c), + } + } + } + } + + pub fn sum_versions(packet: Packet) -> u64 { + match packet { + Packet::Literal(_) => panic!("unexpected literal on root level."), + Packet::Operator(data) => { + data.children + .iter() + .fold(data.header.version, |acc, curr| match curr { + Packet::Literal(child_data) => acc + child_data.header.version, + Packet::Operator(child_data) => { + acc + sum_versions(Packet::Operator(child_data.clone())) + } + }) + } + } + } +} + +use self::decoder::decode; +use self::interpreter::{interpret, sum_versions}; + +pub fn part_one(input: &str) -> u64 { + let packet = decode(input.lines().next().unwrap()); + sum_versions(packet) +} + +pub fn part_two(input: &str) -> u64 { + let packet = decode(input.lines().next().unwrap()); + interpret(packet) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + assert_eq!(part_one("8A004A801A8002F478"), 16); + assert_eq!(part_one("A0016C880162017C3686B18A3D4780"), 31); + assert_eq!(part_one("620080001611562C8802118E34"), 12); + assert_eq!(part_one("C0015000016115A2E0802F182340"), 23); + } + + #[test] + fn test_part_two() { + assert_eq!(part_two("C200B40A82"), 3); + assert_eq!(part_two("04005AC33890"), 54); + assert_eq!(part_two("880086C3E88112"), 7); + assert_eq!(part_two("CE00C43D881120"), 9); + assert_eq!(part_two("D8005AC2A8F0"), 1); + assert_eq!(part_two("F600BC2D8F"), 0); + assert_eq!(part_two("9C005AC2F8F0"), 0); + assert_eq!(part_two("9C0141080250320F1802104A08"), 1); + } +} diff --git a/2022/src/solutions/day17.rs b/2022/src/solutions/day17.rs new file mode 100644 index 0000000..a4f32fe --- /dev/null +++ b/2022/src/solutions/day17.rs @@ -0,0 +1,105 @@ +use std::cmp::max; + +type Point = (isize, isize); +type Velocity = (isize, isize); + +struct Bounds { + left: isize, + right: isize, + top: isize, + bottom: isize, +} + +impl Bounds { + fn contains(&self, (x, y): &Point) -> bool { + x >= &self.left && x <= &self.right && y <= &self.top && y >= &self.bottom + } +} + +fn parse(line: &str) -> Bounds { + let values: Vec = line + .split(',') + .flat_map(|part| { + part.split('=') + .last() + .unwrap() + .split("..") + .map(|x| x.parse().unwrap()) + }) + .collect(); + + Bounds { + left: values[0], + right: values[1], + bottom: values[2], + top: values[3], + } +} + +fn simulate_point( + initial_point: Point, + initial_velocity: Velocity, + bounds: &Bounds, +) -> Option { + let mut point = initial_point; + let mut velocity = initial_velocity; + let mut y_max = point.1; + + // terminate if point has overshot bounds + while point.0 <= bounds.right && point.1 >= bounds.bottom { + point = (point.0 + velocity.0, point.1 + velocity.1); + y_max = max(point.1, y_max); + + if bounds.contains(&point) { + return Some(y_max); + } else { + velocity = (max(0, velocity.0 - 1), velocity.1 - 1); + } + } + + None +} + +fn find_hits(bounds: &Bounds) -> Vec { + let mut max_y = Vec::new(); + let initial_position = (0, 0); + + for x in 0..=bounds.right { + for y in bounds.bottom..=-bounds.bottom { + if let Some(y) = simulate_point(initial_position, (x, y), bounds) { + max_y.push(y); + } + } + } + + max_y +} + +pub fn part_one(input: &str) -> isize { + let bounds = parse(input.lines().next().unwrap()); + *find_hits(&bounds).iter().max().unwrap() +} + +pub fn part_two(input: &str) -> usize { + let bounds = parse(input.lines().next().unwrap()); + find_hits(&bounds).len() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 17); + assert_eq!(part_one(&input), 45); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 17); + assert_eq!(part_two(&input), 112); + } +} diff --git a/2022/src/solutions/day18.rs b/2022/src/solutions/day18.rs new file mode 100644 index 0000000..5d4acdd --- /dev/null +++ b/2022/src/solutions/day18.rs @@ -0,0 +1,187 @@ +use itertools::Itertools; +use std::cmp::max; + +#[derive(Clone, Copy)] +enum Symbol { + Open, + Close, + Comma, + Num(u32), +} + +impl Symbol { + fn from_char(c: char) -> Self { + match c { + '[' => Symbol::Open, + ']' => Symbol::Close, + ',' => Symbol::Comma, + c => Symbol::Num(c.to_digit(10).unwrap()), + } + } +} + +type Snail = Vec; + +fn add(a: &[Symbol], b: &[Symbol]) -> Snail { + let mut snail: Snail = vec![Symbol::Open]; + snail.extend(a.iter().cloned()); + snail.push(Symbol::Comma); + snail.extend(b.iter().cloned()); + snail.push(Symbol::Close); + reduce(&mut snail); + snail +} + +fn from_str(s: &str) -> Snail { + s.chars().map(Symbol::from_char).collect() +} + +fn reduce(snail: &mut Snail) { + while explode(snail) || split(snail) {} +} + +fn split(snail: &mut Snail) -> bool { + let pos = snail.iter().position(|s| match s { + Symbol::Num(x) => *x > 9, + _ => false, + }); + + match pos { + None => false, + Some(index) => { + if let Symbol::Num(x) = snail[index] { + let l = x / 2; + let r = x - l; + snail.splice( + index..index + 1, + [ + Symbol::Open, + Symbol::Num(l), + Symbol::Comma, + Symbol::Num(r), + Symbol::Close, + ], + ); + true + } else { + unreachable!() + } + } + } +} + +fn explode(snail: &mut Snail) -> bool { + let mut depth = 0; + + let pos = snail.iter().position(|s| { + match s { + Symbol::Open => { + depth += 1; + } + Symbol::Close => { + depth -= 1; + } + _ => (), + }; + + depth == 5 + }); + + match pos { + None => false, + Some(index) => { + let l = match snail[index + 1] { + Symbol::Num(x) => x, + _ => unreachable!(), + }; + + let r = match snail[index + 3] { + Symbol::Num(x) => x, + _ => unreachable!(), + }; + + snail[..index].iter_mut().rev().find_map(|x| { + if let Symbol::Num(x) = x { + *x += l; + Some(()) + } else { + None + } + }); + + snail[index + 4..].iter_mut().find_map(|x| { + if let Symbol::Num(x) = x { + *x += r; + Some(()) + } else { + None + } + }); + + snail.splice(index..index + 5, [Symbol::Num(0)]); + true + } + } +} + +fn parse(input: &str) -> Vec { + input.lines().map(from_str).collect() +} + +// this previously used a recursive function based on casting to json. +// found this on a random reddit thread and it is both faster and more elegant. +fn calc_magnitude(snail: &[Symbol]) -> u32 { + let mut multiplier = 1; + let mut output = 0; + + for symbol in snail { + match symbol { + Symbol::Close => multiplier /= 2, + Symbol::Comma => multiplier = (multiplier / 3) * 2, + Symbol::Num(x) => output += *x * multiplier, + Symbol::Open => multiplier *= 3, + } + } + + output +} + +pub fn part_one(input: &str) -> u32 { + calc_magnitude( + &parse(input) + .into_iter() + .fold1(|acc, curr| add(&acc, &curr)) + .unwrap(), + ) +} + +pub fn part_two(input: &str) -> u32 { + parse(input).iter().combinations(2).fold(0, |acc, snails| { + max( + acc, + max( + calc_magnitude(&add(snails[0], snails[1])), + calc_magnitude(&add(snails[1], snails[0])), + ), + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 18); + assert_eq!(part_one(&input), 4140); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 18); + assert_eq!(part_two(&input), 3993); + } +} diff --git a/2022/src/solutions/day19.rs b/2022/src/solutions/day19.rs new file mode 100644 index 0000000..632b4e6 --- /dev/null +++ b/2022/src/solutions/day19.rs @@ -0,0 +1,216 @@ +use itertools::Itertools; +use std::{ + collections::HashSet, + ops::{Add, Sub}, +}; + +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +struct Point(i32, i32, i32); + +impl Add for Point { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0, self.1 + other.1, self.2 + other.2) + } +} + +impl Sub for Point { + type Output = Self; + + fn sub(self, other: Self) -> Self { + Self(self.0 - other.0, self.1 - other.1, self.2 - other.2) + } +} + +impl Point { + fn distance(&self, other: &Point) -> i32 { + (other.0 - self.0).pow(2) + (other.1 - self.1).pow(2) + (other.2 - self.2).pow(2) + } + + fn manhattan_distance(&self, other: &Point) -> i32 { + (self.0 - other.0).abs() + (self.1 - other.1).abs() + (self.2 - other.2).abs() + } + + fn rotate(&self, rot: u8) -> Self { + let &Point(x, y, z) = self; + + match rot { + // translation of http://www.euclideanspace.com/maths/algebra/matrix/transforms/examples/index.htm + 0 => Point(x, y, z), + 1 => Point(x, z, -y), + 2 => Point(x, -y, -z), + 3 => Point(x, -z, y), + 4 => Point(y, -x, z), + 5 => Point(y, z, x), + 6 => Point(y, x, -z), + 7 => Point(y, -z, -x), + 8 => Point(-x, -y, z), + 9 => Point(-x, -z, -y), + 10 => Point(-x, y, -z), + 11 => Point(-x, z, y), + 12 => Point(-y, x, z), + 13 => Point(-y, -z, x), + 14 => Point(-y, -x, -z), + 15 => Point(-y, z, -x), + 16 => Point(z, y, -x), + 17 => Point(z, x, y), + 18 => Point(z, -y, x), + 19 => Point(z, -x, -y), + 20 => Point(-z, -y, -x), + 21 => Point(-z, -x, y), + 22 => Point(-z, y, x), + 23 => Point(-z, x, -y), + v => panic!("unexpected rotation {}", v), + } + } +} + +type Neighbors = (usize, usize); + +type Report = Vec; +type Reports = Vec; + +type Distances = HashSet; + +fn parse(input: &str) -> Reports { + input.lines().fold(Vec::new(), |mut acc, l| { + if l.starts_with("---") { + acc.push(vec![]); + } else if !l.is_empty() { + let mut coords = l.split(',').map(|s| s.parse::().unwrap()); + let last = acc.len() - 1; + + acc[last].push(Point( + coords.next().unwrap(), + coords.next().unwrap(), + coords.next().unwrap(), + )); + } + + acc + }) +} + +fn distances(reports: &[Report]) -> Vec { + reports + .iter() + .map(|r| { + r.iter() + .tuple_combinations() + .map(|(p1, p2)| p1.distance(p2)) + .collect() + }) + .collect() +} + +fn find_neighbors(distances: &[Distances]) -> Vec { + distances + .iter() + .enumerate() + .tuple_combinations() + .filter(|((_, d1), (_, d2))| d1.intersection(d2).count() >= 66) + .flat_map(|((i, _), (j, _))| [(i, j), (j, i)]) + .collect() +} + +fn unaligned_neighbors(neighbors: &[Neighbors], aligned: &[Report]) -> Option { + neighbors + .iter() + .find(|(a, b)| !aligned[*a].is_empty() && aligned[*b].is_empty()) + .map(|(a, b)| (*a, *b)) +} + +fn find_pair_by_distance(reports: &[Point], distance: i32) -> (&Point, &Point) { + reports + .iter() + .tuple_combinations() + .find(|(a, b)| a.distance(b) == distance) + .unwrap() +} + +fn align(reports: &[Report]) -> (Vec, Vec) { + let distances = distances(reports); + let neighbors = find_neighbors(&distances); + + let mut alignments: Vec = vec![Point(0, 0, 0)]; + let mut aligned: Vec = vec![vec![]; reports.len()]; + aligned[0] = reports[0].clone(); + + while let Some((a, b)) = unaligned_neighbors(&neighbors, &aligned) { + let common_distance = distances[a].intersection(&distances[b]).next().unwrap(); + + let (c0, c1) = find_pair_by_distance(&aligned[a], *common_distance); + let (t0, t1) = find_pair_by_distance(&reports[b], *common_distance); + + let mut alignment: Option = None; + let mut rot = 0; + + while rot < 24 { + let r0 = t0.rotate(rot); + let r1 = t1.rotate(rot); + + let saligned = r0 - *c0 == r1 - *c1; + let asaligned = r0 - *c1 == r1 - *c0; + + if saligned || asaligned { + alignment = if saligned { + Some(*c0 - r0) + } else { + Some(*c1 - r0) + }; + break; + } else { + rot += 1; + } + } + + if let Some(alignment) = alignment { + alignments.push(alignment); + aligned[b] = reports[b] + .iter() + .map(|p| p.rotate(rot) + alignment) + .collect(); + } else { + panic!("could not find a canonical orientation for all reports!"); + } + } + + (aligned, alignments) +} + +pub fn part_one(input: &str) -> usize { + let reports = parse(input); + align(&reports).0.iter().flatten().unique().count() +} + +pub fn part_two(input: &str) -> i32 { + let reports = parse(input); + let (_, alignments) = align(&reports); + + alignments + .iter() + .tuple_combinations() + .map(|(a, b)| a.manhattan_distance(b)) + .max() + .unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 19); + assert_eq!(part_one(&input), 79); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 19); + assert_eq!(part_two(&input), 3621); + } +} diff --git a/2022/src/solutions/day20.rs b/2022/src/solutions/day20.rs new file mode 100644 index 0000000..6b0adb0 --- /dev/null +++ b/2022/src/solutions/day20.rs @@ -0,0 +1,105 @@ +type Pixels = Vec; +type Grid = Vec; + +pub fn arr_to_int(bits: &[bool]) -> usize { + bits.iter().fold(0, |acc, &b| acc * 2 + (b as usize)) +} + +fn to_pixels(s: &str) -> Pixels { + s.chars().map(|c| c == '#').collect() +} + +fn parse(input: &str) -> (Pixels, Grid) { + let mut lines = input.lines(); + let cipher = to_pixels(lines.next().unwrap()); + let grid = lines.filter(|l| !l.is_empty()).map(to_pixels).collect(); + + (cipher, grid) +} + +fn pad(grid: &mut Vec, state: bool) { + let empty_line = vec![state; grid.len()]; + grid.insert(0, empty_line.clone()); + grid.push(empty_line); + + for row in grid.iter_mut() { + row.insert(0, state); + row.push(state); + } +} + +fn expand(grid: &mut Vec, cipher: &[bool], state: bool) -> bool { + pad(grid, state); + + let w = grid[0].len(); + let h = grid.len(); + + let mut next_grid = grid.clone(); + + for x in 0..w { + for y in 0..h { + let t = y == 0; + let l = x == 0; + let r = x == w - 1; + let b = y == h - 1; + let id = [ + if !t && !l { grid[y - 1][x - 1] } else { state }, + if !t { grid[y - 1][x] } else { state }, + if !t && !r { grid[y - 1][x + 1] } else { state }, + if !l { grid[y][x - 1] } else { state }, + grid[y][x], + if !r { grid[y][x + 1] } else { state }, + if !b && !l { grid[y + 1][x - 1] } else { state }, + if !b { grid[y + 1][x] } else { state }, + if !b && !r { grid[y + 1][x + 1] } else { state }, + ]; + + next_grid[y][x] = cipher[arr_to_int(&id)]; + } + } + + *grid = next_grid; + cipher[arr_to_int(&[state; 9])] +} + +fn expand_times(input: &str, times: u32) -> Vec { + let (cipher, mut grid) = parse(input); + let mut state = false; + + for _ in 0..times { + state = expand(&mut grid, &cipher, state); + } + + grid +} + +fn count(arr: &[Pixels]) -> usize { + arr.iter().flatten().filter(|&&x| x).count() +} + +pub fn part_one(input: &str) -> usize { + count(&expand_times(input, 2)) +} + +pub fn part_two(input: &str) -> usize { + count(&expand_times(input, 50)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 20); + assert_eq!(part_one(&input), 35); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 20); + assert_eq!(part_two(&input), 3351); + } +} diff --git a/2022/src/solutions/day21.rs b/2022/src/solutions/day21.rs new file mode 100644 index 0000000..247be35 --- /dev/null +++ b/2022/src/solutions/day21.rs @@ -0,0 +1,107 @@ +use std::collections::HashMap; + +fn parse(input: &str) -> Vec { + input + .lines() + .filter(|l| !l.is_empty()) + .map(|l| l.split(": ").last().unwrap().parse::().unwrap()) + .collect() +} + +fn deterministic_roll(position: &mut u64, score: &mut u64, dice: &mut u64) { + let roll: u64 = (*dice..(*dice + 3)).sum(); + let next_position = ((*position + roll - 1) % 10) + 1; + *position = next_position; + *score += next_position; + *dice += 3; +} + +pub fn part_one(input: &str) -> u64 { + let positions = parse(input); + let mut p1_position = positions[0]; + let mut p2_position = positions[1]; + let mut p1_score = 0; + let mut p2_score = 0; + let mut dice = 1; + + loop { + deterministic_roll(&mut p1_position, &mut p1_score, &mut dice); + if p1_score >= 1000 { + return p2_score * (dice - 1); + } + + deterministic_roll(&mut p2_position, &mut p2_score, &mut dice); + if p2_score >= 1000 { + return p1_score * (dice - 1); + } + } +} + +// possible rolls for a 3-sided die. +static ROLLS: [(u64, u64); 7] = [(3, 1), (4, 3), (5, 6), (6, 7), (7, 6), (8, 3), (9, 1)]; + +type Cache = HashMap<(u64, u64, u64, u64), (u64, u64)>; + +fn play( + p1_position: u64, + p2_position: u64, + p1_score: u64, + p2_score: u64, + cache: &mut Cache, +) -> (u64, u64) { + let cache_key = (p1_position, p2_position, p1_score, p2_score); + + // if there is a cached resolution for this game state, return it. + if cache.contains_key(&cache_key) { + return *cache.get(&cache_key).unwrap(); + } + + // the game loop works by swapping p1 & p2 every iteration: + // 1. increment position and score for p1 for every possible dice roll in a turn. + // 2. call play with p1 and p2 swapped since it's now p2's turn. + // 3. if any of the previous positions won, count it as a win (=p2 is previous p1) and return. + // 4. once a loop returns, increment the win counter **in a swapped fashion** since the wins reported for p2 belong to p1. + if p2_score >= 21 { + (0, 1) + } else { + let res = ROLLS.iter().fold((0, 0), |acc, (roll, n)| { + let position = ((p1_position + roll - 1) % 10) + 1; + let wins = play(p2_position, position, p2_score, p1_score + position, cache); + (acc.0 + n * wins.1, acc.1 + n * wins.0) + }); + + cache.insert(cache_key, res); + + res + } +} + +pub fn part_two(input: &str) -> u64 { + let mut cache = HashMap::new(); + + let positions = parse(input); + let p1_position = positions[0]; + let p2_position = positions[1]; + + let (p1_wins, p2_wins) = play(p1_position, p2_position, 0, 0, &mut cache); + std::cmp::max(p1_wins, p2_wins) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 21); + assert_eq!(part_one(&input), 739785); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 21); + assert_eq!(part_two(&input), 444356092776315); + } +} diff --git a/2022/src/solutions/day22.rs b/2022/src/solutions/day22.rs new file mode 100644 index 0000000..d5486de --- /dev/null +++ b/2022/src/solutions/day22.rs @@ -0,0 +1,131 @@ +use std::cmp::{max, min}; + +#[derive(Clone)] +struct Range { + from: i64, + to: i64, +} + +#[derive(Clone)] +struct Ranges { + x: Range, + y: Range, + z: Range, +} + +#[derive(Clone)] +struct Cube { + on: bool, + ranges: Ranges, +} + +fn parse_range(s: &str) -> Range { + let mut range = s.split('=').last().unwrap().split(".."); + let from = range.next().unwrap().parse().unwrap(); + let to = range.next().unwrap().parse().unwrap(); + + Range { from, to } +} + +fn parse(input: &str) -> Vec { + input + .lines() + .map(|l| { + let on = l.starts_with("on"); + let mut ranges = l.split(' ').last().unwrap().split(',').map(parse_range); + + let x = ranges.next().unwrap(); + let y = ranges.next().unwrap(); + let z = ranges.next().unwrap(); + let ranges = Ranges { x, y, z }; + + Cube { on, ranges } + }) + .collect() +} + +fn intersect(a: &Range, b: &Range) -> Option { + if b.from > a.to || a.from > b.to { + None + } else { + Some(Range { + from: max(a.from, b.from), + to: min(a.to, b.to), + }) + } +} + +fn intersection(a: &Cube, b: &Cube, on: bool) -> Option { + let x = intersect(&a.ranges.x, &b.ranges.x)?; + let y = intersect(&a.ranges.y, &b.ranges.y)?; + let z = intersect(&a.ranges.z, &b.ranges.z)?; + + Some(Cube { + on, + ranges: Ranges { x, y, z }, + }) +} + +fn cube_diffs(instructions: Vec) -> Vec { + instructions.iter().fold(Vec::new(), |mut acc, curr| { + // add `on` instructions to the diff list. + let mut to_add = if curr.on { vec![curr.clone()] } else { vec![] }; + // for every intersect with a previous diff, add a diff with opposite sign. + // this works because we only track `on` cubes and subtractions to them by default: + // 1. `off` instructions turn off intersecting parts of previous diffs. + // 2. previously `off` diffs can be turned back on by `on` instructions. + // 3. if both instructions were `on`, the new instruction cancel out old diffs to prevent duplicates. + to_add.extend(acc.iter().filter_map(|c| intersection(curr, c, !c.on))); + acc.extend(to_add); + acc + }) +} + +fn vol(r: Range) -> i64 { + r.to - r.from + 1 +} + +fn volume(c: Cube) -> i64 { + let sign = if c.on { 1 } else { -1 }; + sign * vol(c.ranges.x) * vol(c.ranges.y) * vol(c.ranges.z) +} + +pub fn part_one(input: &str) -> i64 { + let bounds = Cube { + on: true, + ranges: Ranges { + x: Range { from: -50, to: 50 }, + y: Range { from: -50, to: 50 }, + z: Range { from: -50, to: 50 }, + }, + }; + + cube_diffs(parse(input)) + .into_iter() + .filter_map(|c| intersection(&c, &bounds, c.on)) + .map(volume) + .sum() +} + +pub fn part_two(input: &str) -> i64 { + cube_diffs(parse(input)).into_iter().map(volume).sum() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 22); + assert_eq!(part_one(&input), 474140); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 22); + assert_eq!(part_two(&input), 2758514936282235); + } +} diff --git a/2022/src/solutions/day23.rs b/2022/src/solutions/day23.rs new file mode 100644 index 0000000..db50246 --- /dev/null +++ b/2022/src/solutions/day23.rs @@ -0,0 +1,28 @@ +/// Solved today by hand on a whiteboard with my family. +/// This file contains a helper to sum moves and the correct solutions for my input. +/// Here are some pictures of the whiteboard: +/// [#1](https://user-images.githubusercontent.com/1682504/147255802-bf21c955-7a1f-412f-9cb0-05627d359635.jpeg) +/// [#2](https://user-images.githubusercontent.com/1682504/147255905-00f1ac8a-3d5b-4c01-b310-a1a2655a77f4.jpeg) + +fn sum(l: &str, factor: u64) -> u64 { + l.split(' ') + .map(|x| x.parse::().unwrap() * factor) + .sum() +} + +fn add_lines(pink: &str, blue: &str, green: &str, purple: &str) -> u64 { + sum(pink, 1) + sum(blue, 10) + sum(green, 100) + sum(purple, 1000) +} + +pub fn part_one(_: &str) -> u64 { + add_lines("3 3 5 8", "2 3 5", "2 3 4", "9 9") +} + +pub fn part_two(_: &str) -> u64 { + add_lines( + "8 8 4 5 5 5 9 9", + "7 4 5 8 7 7", + "7 2 5 6 5 6", + "11 11 11 11", + ) +} diff --git a/2022/src/solutions/day24.rs b/2022/src/solutions/day24.rs new file mode 100644 index 0000000..72e3bae --- /dev/null +++ b/2022/src/solutions/day24.rs @@ -0,0 +1,80 @@ +use std::collections::HashMap; + +fn calculate_step(w: i64, z: i64, a: i64, b: i64, c: i64) -> i64 { + let x = ((z % 26 + b) != w) as i64; + (z / a) * (25 * x + 1) + ((w + c) * x) +} + +fn solve() -> Vec { + let steps = [ + (1, 13, 10), + (1, 11, 16), + (1, 11, 0), + (1, 10, 13), + (26, -14, 7), + (26, -4, 11), + (1, 11, 11), + (26, -3, 10), + (1, 12, 16), + (26, -12, 8), + (1, 13, 15), + (26, -12, 2), + (26, -15, 5), + (26, -12, 10), + ]; + + let mut step = 0; + let mut z_values: HashMap> = HashMap::new(); + + for w in 1..=9 { + let (a, b, c) = steps[step]; + z_values + .entry(calculate_step(w, 0, a, b, c)) + .or_default() + .push(w); + } + + step += 1; + + while step < 14 { + let values: Vec<(i64, Vec)> = z_values.drain().collect(); + + values.iter().for_each(|(z, nums)| { + let (a, b, c) = steps[step]; + + for w in 1..=9 { + let next_z = calculate_step(w, *z, a, b, c); + // optimization: remove z values above threshold. + // optimization: remove z values that do not shrink when divided / 26. + if (a == 1 || next_z < *z) && next_z < 1000000 { + let c = w.to_string().chars().next().unwrap(); + let entry = z_values.entry(next_z).or_default(); + + let next_nums: Vec = nums + .iter() + .map(|n| { + let mut digits: Vec = n.to_string().chars().collect(); + digits.push(c); + digits.iter().collect::().parse().unwrap() + }) + .collect(); + + entry.push(*next_nums.iter().min().unwrap()); + entry.push(*next_nums.iter().max().unwrap()); + } + } + }); + + step += 1; + } + + z_values.get(&0).unwrap().to_owned() +} + +pub fn part_one(_: &str) -> i64 { + *solve().iter().max().unwrap() +} + +pub fn part_two(_: &str) -> i64 { + *solve().iter().min().unwrap() +} diff --git a/2022/src/solutions/day25.rs b/2022/src/solutions/day25.rs new file mode 100644 index 0000000..5a80b10 --- /dev/null +++ b/2022/src/solutions/day25.rs @@ -0,0 +1,109 @@ +#[derive(Clone)] +enum Occupant { + EastBound, + SouthBound, + Empty, +} + +type Line = Vec; + +fn parse(input: &str) -> Vec { + input + .lines() + .filter_map(|l| { + if l.is_empty() { + None + } else { + Some( + l.chars() + .map(|c| match c { + '>' => Occupant::EastBound, + 'v' => Occupant::SouthBound, + '.' => Occupant::Empty, + c => panic!("unexpected input: {}", c), + }) + .collect(), + ) + } + }) + .collect() +} + +fn simulate_step(grid: &mut Vec) -> u32 { + let mut moved = 0; + + let w = grid[0].len(); + let h = grid.len(); + + let reference = grid.clone(); + // eastbound traffic + for y in 0..h { + for x in 0..w { + let x2 = if x == 0 { w - 1 } else { x - 1 }; + + if let Occupant::Empty = reference[y][x] { + if let Occupant::EastBound = reference[y][x2] { + grid[y][x2] = Occupant::Empty; + grid[y][x] = Occupant::EastBound; + moved += 1; + } + } + } + } + + let reference = grid.clone(); + // southbound traffic + for y in 0..h { + for x in 0..w { + let y2 = if y == 0 { h - 1 } else { y - 1 }; + + if let Occupant::Empty = reference[y][x] { + if let Occupant::SouthBound = reference[y2][x] { + grid[y2][x] = Occupant::Empty; + grid[y][x] = Occupant::SouthBound; + moved += 1; + } + } + } + } + + moved +} + +pub fn part_one(input: &str) -> u32 { + let mut grid = parse(input); + let mut step = 0; + + loop { + step += 1; + + if simulate_step(&mut grid) == 0 { + break; + } + } + + step +} + +pub fn part_two(_input: &str) -> u32 { + 0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + use aoc::read_file; + let input = read_file("examples", 25); + assert_eq!(part_one(&input), 58); + } + + #[test] + fn test_part_two() { + use aoc::read_file; + let input = read_file("examples", 25); + assert_eq!(part_two(&input), 0); + } +} diff --git a/2022/src/solutions/mod.rs b/2022/src/solutions/mod.rs new file mode 100644 index 0000000..d5bc06f --- /dev/null +++ b/2022/src/solutions/mod.rs @@ -0,0 +1,25 @@ +pub mod day01; +pub mod day02; +pub mod day03; +pub mod day04; +pub mod day05; +pub mod day06; +pub mod day07; +pub mod day08; +pub mod day09; +pub mod day10; +pub mod day11; +pub mod day12; +pub mod day13; +pub mod day14; +pub mod day15; +pub mod day16; +pub mod day17; +pub mod day18; +pub mod day19; +pub mod day20; +pub mod day21; +pub mod day22; +pub mod day23; +pub mod day24; +pub mod day25;