{"id":16301159,"url":"https://github.com/softmoth/advent2021","last_synced_at":"2025-10-20T01:05:18.374Z","repository":{"id":147468543,"uuid":"528089871","full_name":"softmoth/advent2021","owner":"softmoth","description":"Advent of Code 2021 exercises in Rust","archived":false,"fork":false,"pushed_at":"2022-09-15T20:50:20.000Z","size":145,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-10T01:16:41.904Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/softmoth.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-23T17:10:26.000Z","updated_at":"2022-08-23T17:11:28.000Z","dependencies_parsed_at":"2023-07-24T19:32:22.813Z","dependency_job_id":null,"html_url":"https://github.com/softmoth/advent2021","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softmoth%2Fadvent2021","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softmoth%2Fadvent2021/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softmoth%2Fadvent2021/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/softmoth%2Fadvent2021/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/softmoth","download_url":"https://codeload.github.com/softmoth/advent2021/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248137887,"owners_count":21053775,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-10T20:54:01.013Z","updated_at":"2025-10-20T01:05:13.341Z","avatar_url":"https://github.com/softmoth.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Advent of Code 2021 exercises\n\nThe main site is https://adventofcode.com/2021[].\n\nI was inspired to do it by\nhttps://www.youtube.com/playlist?list=PLWtPciJ1UMuAJ-To7dMk71e-aiwBLg_Id[Chris Biscardi].\n\n\n## Day 1\n\nI learned `std::io::stdin().lines()`, which handles locking automatically in recent Rust.\n\nChris Biscardi introduced me to *dhat*, a very simple heap profiler. So far, that along with `/usr/bin/time` is enough to do some basic testing.\n\n\n## Day 2\n\nI rewrote this several times. First with a naïve `split_whitespace()` parser, and then using `nom` (introduced by Chris Biscardi). Basically I copied his approach verbatim.\n\nI did learn how to return an enum value from a function, and then call it later. The trick is to cast the enum value explicitly to `fn(i32) -\u003e Command`. Otherwise it looks like `fn(i32) -\u003e Command {Command::Down}` (i.e., it's too specific) and the compiler thinks each value is of a different type. That was satisfying to get working.\n\n\n## Day 3\n\nWorking with BTreeSet was interesting. For part 1 (solution not kept) I just kept a count of how many entries had each bit set. But part 2 required storing all of the entries and whittling them down. At first I tried `.filter_drain()`, but then I found that an initial `.partition()` of the set allows the two questions to be answered without needing to duplicate any of the entries.\n\n\n## Day 4\n\nThis is my first time actually working with *ndarray*. Once I figured out the basics, it was trivial to use `board.axis_iter()` to check both across and down. So it worked out well, and was good to get my feet wet with the library.\n\nI'm starting to get used to *nom* for parsing text, and feel like I can get it into a data structure fairly efficiently, but I'm guessing there's a better way still. At first I used nested for loops and explicit indexing into a `[u8;BOARD_SIZE]` fixed array. I had to use `fold_many_m_n()` in the inner loop to avoid `nom` allocating an intermediate `Vec`. Using vectors instead isn't too bad; it adds about 50 bytes of allocation per board parsed. Mapping that with `Array::from_shape_vec()` adds another 200 bytes or so.\n\nBut it took me a while to get to compile with each change of organization. It's good practice for managing ownership, etc.\n\nPart 2 was pretty straightforward. Instead of stopping after the first win, just process all of them. I kept every board that won, in order, then it's easy to grab the first and last for the answer.\n\n\n## Day 5\n\nPart one uses similar techniques to Day 4. Parsing with nom continues to be pleasant. I'm getting more familiar with ndarray.\n\nMost of the awkwardness is that I'm still learning how to specify the types when needed, how to use the libraries, and so forth. For example, I spent a fair amount of time trying to understand why `.map_inplace()` wasn't getting a mutable ref to the value, even though I'd declared the array as `mut`. I realized I needed `.slice_mut()` instead of `.slice()`, which would be obvious to a more seasoned Rust developer.\n\nI did a little bit of code organizing, while still keeping each day self-contained. Now `main` just slurps the input and shows the result of a `process()` function. All the guts are below the fold.\n\nPart two doesn't add much interesting. I can't find a way to iterate over a diagonal slice in ndarray (`SliceInfoElem` sensibly doesn't handle that), so I just write the imperative loop explicitly.\n\n\n## Day 6\n\nFor part 1, the parsing was trivial. The solution was pretty clear, but I wanted to avoid unnecessary copying, so instead of shifting or rotating a Vec of numbers, I made my own data structure to track the index. This means there's only a few simple additions needed, rather than moving N values. I guess with only 8 lifetimes, the copying would be minimal, but this felt right.\n\nI thought of implementing Iterator for it, so I could do `school.iter().take(80).tail()` or similar, but that didn't really work out.\n\nFor part 2, I had to use u64 for all the counts. I was using --release to build, so I didn't even get a panic at runtime. I've switched to use `.checked_add()` to catch it in `.advance()`, but the final `.sum()` isn't checked and can still overflow.\n\nI extended the test to 442 days, which is the max that will fit into a u64 result: 18_353_315_898_976_047_013 fish.\n\nAlso, clippy is great! It found a place where `.ok_or()` was unconditionally creating an Err() object that was never used, and recommended `.ok_or_else()` instead. It wasn't exactly a memory leak, but was ruining my dhat score. `:-)`\n\nSo it's safe to say that this was a good learning experience! Remember that `--release` isn't just about optimization level! Rust makes the tools to check when needed very easy to use.\n\n\n# Day 7\n\nI started out trying to use ndarray-stats, thinking there must be some statistical method that would just give me the right answer. But I couldn't find such a thing.\n\nSo I decided to just brute force it, and it turns out to be pretty quick regardless. The Rust bits aren't too interesting, although it was again good practice for working with different integer types (e.g., I didn't know about `.unsigned_abs()` before).\n\n\n# Day 8\n\nPart 1 went pretty smoothly, just using strings for storing the segments. Converting it to use `bitvec` instead took a bit of working out as I've never used that library before. It was pretty trivial once I did find out how to create the `BitArr!()` type and so forth. And the values can be used with bitwise arithmetic operators, so that's perfect.\n\nPart 2 is much more of a learning opportunity. Especially it's been fiddly in getting the right dereferencing of values inside of filter closures, etc. Doing higher-order functions is still tricky for me; I know exactly what I want to do, but getting the right levels of indirection and the right type signatures is fiddly. It's good practice.\n\nMy solution is alright, but I feel like I might be missing some underlying organization that could make it much better.\n\n\n# Day 9\n\nThis day I spent a lot of time trying out `impl\u003c'a\u003e Index for Heightmap\u003c'a\u003e`,  before abandoning it and just using a `.at((x, y))` method instead. Since I wanted to try using a reference to the input itself as the data, I wanted `.at()` to convert the ASCII values to numbers on the fly, and this isn't possible with Index. Index has to return a reference into the data, it can't return an owned value.\n\nOther than that, the implementation went fairly smoothly. The Itertools `.cartesian_product` method is a nice luxury.\n\n\n# Day 10\n\nI have a compulsion to do everything in one pass where possible. I even tried the `apply` crate, to structure `process()` with no top-level `let` statements. But it was a bit too cute, and I think it's more legible in this case without it.\n\nI still find the guts of this to be hard to read. Pulling some of that out into smaller functions would help, but the worst of it is the match statement in `check_line()`, and it is hard to simplify. Maybe the SyntaxCheck type isn't the best fundamental representation for the state.\n\n\n# Day 11\n\nI spent most of my time on this trying to get a `.neighbors8_mut()` Iterator impl working on my DigitGrid struct. I gave up on that after quite a bit of messing about and reading through much of the https://rust-unofficial.github.io/too-many-lists/[Too Many Lists] book again.\n\nThen I had an off-by-one error that took me a while to find, so I was firing the octopus when it hit 9 rather than 10. I suspected my neighbor code and everything else before I realized it was just I'd read the problem wrong.\n\nPart 2 was easy once part 1 was done.\n\nI was not thrilled with the heap performance. The max usage was under 5K, but total allocated was 2M, largely from the neighbors() function collecting into a Vec just so it can stuff it into an array. Creating the array on the stack and just filling it manually got it down to 900K, and sped it up considerably.\nI think most of the allocation is now in\n`step()`'s\n`VecDeque`, which makes sense.\n\n\n# Day 12\n\nI kept getting stuck trying to formulate the iterative version directly for some reason. So I finally took a break and coded a recursive version in Perl. Once I had that working, I coded the same thing in Rust. The result used 7.5M of memory and made 150M total allocations, and took almost 4 times as long as the Perl version. Hmmm... TIMTOWTDI?\n\nAs for the memory, that looks like less than the Perl version. The 4x time increase is due to dhat; without instrumentation it's significantly faster than Perl's version. This took me a while to figure out, though. So, as should be expected, if there are enough allocations happening then dhat has a big performance impact.\n\nConverting it to an iterative solution went smoothly once I had the recursive version working. Somehow I just was unwilling to put so many clones of paths on the stack, but that is how it's done. Instead of `.clone(), .push()`, an immutable list could just push new head's all sharing the same tail. Maybe a CoW list of this sort exists in Rust, but I didn't find it.\n\n\n# Day 13\n\nFinally, this one went smoothly. A HashSet represents the sparse grid, and while there is some duplication of logic between the fold-left and fold-up cases, it's simple enough that factoring it out into some object doesn't make sense, I think. I'm going to take this one as a win and move on.\n\n\n# Day 14\n\nI did Part 1 with a full Vec representation of the polymer, and it is fine for the 10 steps. It of course failed badly on steps \u003e 20 or so.\n\n\n# Day 15\n\nI first coded up a naive search implementation. See misc/day15-naive.pl. It works for the small test sample, but is overwhelmed by even the Part 1 problem size. It's just too wasteful.\n\nOf course, the real solution is to use A*. The petgraph crate makes this easy. It uses 4M of RAM for Part 1, 85M for the 5x5 Part 2, and 1G of RAM for a 15x15 repeat of Part 2. It'd be nice to see how much better a custom A* implementation for just this problem could be.\n\nMy custom A* implementation does save significantly on memory usage, but winds up using more CPU, so the timings are about equal. But I was able to do a 50x50 grid with 2G of memory allocations (under 1G max size), and it could be optimized more I'm sure.\n\n// vim:set spell tw=0:\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoftmoth%2Fadvent2021","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoftmoth%2Fadvent2021","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoftmoth%2Fadvent2021/lists"}