mirror of
https://github.com/mfocko/blog.git
synced 2024-11-22 13:03:47 +01:00
blog(aoc-2022): add 3rd week
Signed-off-by: Matej Focko <me@mfocko.xyz>
This commit is contained in:
parent
82f1269fa9
commit
5cfa9bd729
1 changed files with 484 additions and 0 deletions
484
blog/aoc-2022/03-week-3.md
Normal file
484
blog/aoc-2022/03-week-3.md
Normal file
|
@ -0,0 +1,484 @@
|
|||
---
|
||||
title: 3rd week of Advent of Code '22 in Rust
|
||||
description: Surviving third week in Rust.
|
||||
date: 2023-07-06T21:00
|
||||
slug: aoc-2022/3rd-week
|
||||
authors: mf
|
||||
tags:
|
||||
- aoc-2022
|
||||
- advent-of-code
|
||||
- rust
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||
Let's go through the third week of [_Advent of Code_] in Rust.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
## [Day 15: Beacon Exclusion Zone](https://adventofcode.com/2022/day/15)
|
||||
|
||||
:::info tl;dr
|
||||
|
||||
Triangulating a distress beacon based on the information from the sensors.
|
||||
|
||||
:::
|
||||
|
||||
### Solution
|
||||
|
||||
Relatively easy thing to implement, no major Rust issues hit.
|
||||
|
||||
## [Day 16: Proboscidea Volcanium](https://adventofcode.com/2022/day/16)
|
||||
|
||||
:::info tl;dr
|
||||
|
||||
Finding a max flow in a graph given some time constraints.
|
||||
|
||||
:::
|
||||
|
||||
### Solution
|
||||
|
||||
I have used some interesting things to implement this and make it easier for me.
|
||||
|
||||
#### Indexing in graph
|
||||
|
||||
I have come across a situation where I needed to keep more information regarding
|
||||
the graph… In that case you can, of course, create a structure and keep it in,
|
||||
but once you have multiple members in the structure it gets harder to work with
|
||||
since you need to address the fields in the structure. When you work with graph,
|
||||
you frequently need to access the vertices and in this case it felt a lot easier
|
||||
to implement the indexing in a graph, rather than explicitly access the
|
||||
underlying data structure.
|
||||
|
||||
Here you can see a rather short snippet from the solution that allows you to
|
||||
“index” the graph:
|
||||
```rust
|
||||
impl Index<&str> for Graph {
|
||||
type Output = Vertex;
|
||||
|
||||
fn index(&self, index: &str) -> &Self::Output {
|
||||
&self.g[index]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Cartesian product
|
||||
|
||||
During the implementation I had to utilize Floyd-Warshall algorithm for finding
|
||||
the shortest path between pairs of vertices and utilized the `iproduct!` macro
|
||||
from the [`itertools`]. It is a very useful higher-order function that allows
|
||||
you to keep the nesting of the loops at a minimum level while still maintaining
|
||||
the same functionality.
|
||||
|
||||
#### “Implementing” an iterator
|
||||
|
||||
For the second part, you get to split the work between 2 actors. That way you
|
||||
can achieve higher efficiency of the whole process that you're planning, but it
|
||||
also makes it harder to evaluate algorithmically, since you need to check the
|
||||
different ways the work can be split.
|
||||
|
||||
Being affected by _functional programming brain damage_:tm:, I have chosen to
|
||||
do this part by function that returns an iterator over the possible ways:
|
||||
```rust
|
||||
fn pairings(
|
||||
valves: &BTreeSet<String>,
|
||||
) -> impl Iterator<Item = (BTreeSet<String>, BTreeSet<String>)> + '_ {
|
||||
let mapping = valves.iter().collect_vec();
|
||||
|
||||
let max_mask = 1 << (valves.len() - 1);
|
||||
|
||||
(0..max_mask).map(move |mask| {
|
||||
let mut elephant = BTreeSet::new();
|
||||
let mut human = BTreeSet::new();
|
||||
|
||||
for (i, &v) in mapping.iter().enumerate() {
|
||||
if (mask & (1 << i)) == 0 {
|
||||
human.insert(v.clone());
|
||||
} else {
|
||||
elephant.insert(v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
(human, elephant)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## [Day 17: Pyroclastic Flow](https://adventofcode.com/2022/day/17)
|
||||
|
||||
:::info tl;dr
|
||||
|
||||
Simulating an autonomous Tetris where pieces get affected by a series of jets of
|
||||
hot gas.
|
||||
|
||||
:::
|
||||
|
||||
### Solution
|
||||
|
||||
Similarly to the previous day I have created some iterators :smile:
|
||||
|
||||
#### Collision detection
|
||||
|
||||
Once you need to check for collisions it is very helpful to be able to just
|
||||
iterate through the positions that can actually collide with the wall or other
|
||||
piece.
|
||||
|
||||
To get the desired behaviour, you can just compose few smaller functions:
|
||||
```rust
|
||||
fn occupied(shape: &[Vec<char>]) -> impl Iterator<Item = Position> + '_ {
|
||||
shape.iter().enumerate().flat_map(|(y, row)| {
|
||||
row.iter().enumerate().filter_map(move |(x, c)| {
|
||||
if c == &'#' {
|
||||
Some(Vector2D::new(x as isize, y as isize))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
In the end, we get relative positions which we can adjust later when given the
|
||||
specific positions from iterator. You can see some interesting parts in this:
|
||||
|
||||
* `.enumerate()` allows us to get both the indices (coordinates) and the line
|
||||
or, later on, the character itself,
|
||||
* `.flat_map()` flattens the iterator, i.e. when we return another iterator,
|
||||
they just get chained instead of iterating over iterators (which sounds pretty
|
||||
disturbing, doesn't it?),
|
||||
* and finally `.filter_map()` which is pretty similar to the “basic” `.map()`
|
||||
with a one, key, difference that it expects the items of an iterator to be
|
||||
mapped to an `Option<T>` from which it ignores nothing (as in `None` :wink:)
|
||||
and also unwraps the values from `Some(…)`.
|
||||
|
||||
#### Infinite iterator
|
||||
|
||||
In the solution we cycle through both Tetris-like shapes that fall down and the
|
||||
jets that move our pieces around. Initially I have implemented my own infinite
|
||||
iterator that just yields the indices. It is a very simple, yet powerful, piece
|
||||
of code:
|
||||
```rust
|
||||
struct InfiniteIndex {
|
||||
size: usize,
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl InfiniteIndex {
|
||||
fn new(size: usize) -> InfiniteIndex {
|
||||
InfiniteIndex { size, i: size - 1 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for InfiniteIndex {
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.i = (self.i + 1) % self.size;
|
||||
Some(self.i)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However when I'm looking at the code now, it doesn't really make much sense…
|
||||
Guess what, we can use a built-in function that is implemented on iterators for
|
||||
that! The function is called `.cycle()`
|
||||
|
||||
On the other hand, I am not going to switch to that function, since it would
|
||||
introduce an another myriad of issues caused by the fact that I create iterators
|
||||
right away in the constructor of my structure and the iterators would borrow
|
||||
both the jets and shapes which would introduce a lifetime dependency into the
|
||||
structure.
|
||||
|
||||
## [Day 18: Boiling Boulders](https://adventofcode.com/2022/day/18)
|
||||
|
||||
:::info tl;dr
|
||||
|
||||
Let's compute a surface area of some obsidian approximated via coordinates of
|
||||
cubes.
|
||||
|
||||
:::
|
||||
|
||||
### Solution
|
||||
|
||||
This day is kinda interesting, because it shows how easily you can complicate the
|
||||
problem and also how much can you screw yourself over with the optimization and
|
||||
„smart“ approach.
|
||||
|
||||
For the first part you need to find the surface area of an obsidian that is
|
||||
approximated by cubes. Now, that is a very easy thing to do, just keep the track
|
||||
of already added cubes, and check if the newly added cube touches any face of any
|
||||
other cube. Simple, and with a `BTreeSet` relatively efficient way to do it.
|
||||
|
||||
However the second part lets you on a secret that there may be some surface area
|
||||
from the „inside“ too and you want to know only the one from the outside of the
|
||||
obsidian. I have seen some solutions later, but if you check your data, you might
|
||||
notice that the bounding box of all the cubes isn't that big at all. Therefore I
|
||||
chose to pre-construct the box beforehand, fill in the cubes and then just run a
|
||||
BFS turning all the lava on the outside into the air. Now you just need to check
|
||||
cubes and count how many of their faces touch the air.
|
||||
|
||||
## [Day 19: Not Enough Minerals](https://adventofcode.com/2022/day/19)
|
||||
|
||||
:::info tl;dr
|
||||
|
||||
Finding out the best strategy for building robots to collect geodes.
|
||||
|
||||
:::
|
||||
|
||||
### Solution
|
||||
|
||||
Not much interesting stuff to mention apart from the suggestion to never believe
|
||||
that the default implementation given by `derive` macro is what you want, it
|
||||
doesn't have to be. :smile:
|
||||
|
||||
## [Day 20: Grove Positioning System](https://adventofcode.com/2022/day/20)
|
||||
|
||||
:::info tl;dr
|
||||
|
||||
Shuffling around the _circular linked list_ to find the coordinates.
|
||||
|
||||
:::
|
||||
|
||||
Now, small rant for this day is in place. They've never mentioned that coordinates
|
||||
can repeat and therefore the values are non-unique. This is something that did
|
||||
not happen in the given sample, but was present in the user input. It took »a lot«
|
||||
to realize that this is the issue.
|
||||
|
||||
### Solution
|
||||
|
||||
I have tried implementing a circular linked list for this… and I have failed
|
||||
miserably. To be fair, I still have no clue why. It was „fun“ to play around with
|
||||
the `Rc<RefCell<T>>`. In the end I failed on _wrong answer_. I have also encountered
|
||||
a rather interesting issue with `.borrow_mut()` method being used on `Rc<RefCell<T>>`.
|
||||
|
||||
#### `.borrow_mut()`
|
||||
|
||||
Consider the following snippet of the code (taken from the documentation):
|
||||
```rust
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
// use std::borrow::BorrowMut;
|
||||
|
||||
fn main() {
|
||||
let shared_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new()));
|
||||
// Create a new block to limit the scope of the dynamic borrow
|
||||
{
|
||||
let mut map: RefMut<_> = shared_map.borrow_mut();
|
||||
map.insert("africa", 92388);
|
||||
map.insert("kyoto", 11837);
|
||||
map.insert("piccadilly", 11826);
|
||||
map.insert("marbles", 38);
|
||||
}
|
||||
|
||||
// Note that if we had not let the previous borrow of the cache fall out
|
||||
// of scope then the subsequent borrow would cause a dynamic thread panic.
|
||||
// This is the major hazard of using `RefCell`.
|
||||
let total: i32 = shared_map.borrow().values().sum();
|
||||
println!("{total}");
|
||||
}
|
||||
```
|
||||
|
||||
We allocate a hash map on the heap and then in the inner block, we borrow it as
|
||||
a mutable reference, so that we can use it.
|
||||
|
||||
:::note
|
||||
|
||||
It is a very primitive example for `Rc<RefCell<T>>` and mutable borrow.
|
||||
|
||||
:::
|
||||
|
||||
If you uncomment the 4th line with `use std::borrow::BorrowMut;`, you cannot
|
||||
compile the code anymore, because of
|
||||
```
|
||||
Compiling playground v0.0.1 (/playground)
|
||||
error[E0308]: mismatched types
|
||||
--> src/main.rs:10:34
|
||||
|
|
||||
10 | let mut map: RefMut<_> = shared_map.borrow_mut();
|
||||
| --------- ^^^^^^^^^^^^^^^^^^^^^^^ expected struct `RefMut`, found mutable reference
|
||||
| |
|
||||
| expected due to this
|
||||
|
|
||||
= note: expected struct `RefMut<'_, _>`
|
||||
found mutable reference `&mut Rc<RefCell<HashMap<_, _>>>`
|
||||
|
||||
error[E0599]: no method named `insert` found for struct `RefMut<'_, _>` in the current scope
|
||||
--> src/main.rs:11:13
|
||||
|
|
||||
11 | map.insert("africa", 92388);
|
||||
| ^^^^^^ method not found in `RefMut<'_, _>`
|
||||
|
||||
error[E0599]: no method named `insert` found for struct `RefMut<'_, _>` in the current scope
|
||||
--> src/main.rs:12:13
|
||||
|
|
||||
12 | map.insert("kyoto", 11837);
|
||||
| ^^^^^^ method not found in `RefMut<'_, _>`
|
||||
|
||||
error[E0599]: no method named `insert` found for struct `RefMut<'_, _>` in the current scope
|
||||
--> src/main.rs:13:13
|
||||
|
|
||||
13 | map.insert("piccadilly", 11826);
|
||||
| ^^^^^^ method not found in `RefMut<'_, _>`
|
||||
|
||||
error[E0599]: no method named `insert` found for struct `RefMut<'_, _>` in the current scope
|
||||
--> src/main.rs:14:13
|
||||
|
|
||||
14 | map.insert("marbles", 38);
|
||||
| ^^^^^^ method not found in `RefMut<'_, _>`
|
||||
|
||||
Some errors have detailed explanations: E0308, E0599.
|
||||
For more information about an error, try `rustc --explain E0308`.
|
||||
error: could not compile `playground` due to 5 previous errors
|
||||
```
|
||||
|
||||
It might seem **a bit** ridiculous. However, I got to a point where the compiler
|
||||
suggested `use std::borrow::BorrowMut;` and it resulted in breaking parts of the
|
||||
code that worked previously. I think it may be a good idea to go over what is
|
||||
happening here.
|
||||
|
||||
##### `.borrow_mut()` on `Rc<RefCell<T>>`
|
||||
|
||||
Let's consider a variable `x` of type `Rc<RefCell<T>>`. What happens when you
|
||||
call `.borrow_mut()` on it? We can look at the `Rc` type, and… hang on! There is
|
||||
neither `.borrow_mut()` method or `BorrowMut` trait implemented. How can we do it
|
||||
then?
|
||||
|
||||
Let's go further and we can see that `RefCell<T>` implements a `.borrow_mut()`
|
||||
method. OK, but how can we call it on the `Rc<T>`? Easily! `Rc<T>` implements
|
||||
`Deref<T>` and therefore you can call methods on `Rc<T>` objects as if they were
|
||||
`T` objects. If we read on _`Deref` coercion_, we can see the following:
|
||||
|
||||
> If `T` implements `Deref<Target = U>`, …:
|
||||
> * …
|
||||
> * `T` implicitly implements all the (immutable) methods of the type `U`.
|
||||
|
||||
What is the requirement for the `.borrow_mut()` on `RefCell<T>`? Well, it needs
|
||||
`&self`, so the `Deref` implements the `.borrow_mut()` for the `Rc<RefCell<T>>`.
|
||||
|
||||
##### `BorrowMut` trait
|
||||
|
||||
I have not been able to find a lot on this trait. My guess is that it provides a
|
||||
method instead of a syntactic sugar (`&mut x`) for the mutable borrow. And also
|
||||
it provides default implementations for the types:
|
||||
```rust
|
||||
impl BorrowMut<str> for String
|
||||
|
||||
impl<T> BorrowMut<T> for &mut T
|
||||
where
|
||||
T: ?Sized,
|
||||
|
||||
impl<T> BorrowMut<T> for T
|
||||
where
|
||||
T: ?Sized,
|
||||
|
||||
impl<T, A> BorrowMut<[T]> for Vec<T, A>
|
||||
where
|
||||
A: Allocator,
|
||||
|
||||
impl<T, A> BorrowMut<T> for Box<T, A>
|
||||
where
|
||||
A: Allocator,
|
||||
T: ?Sized,
|
||||
|
||||
impl<T, const N: usize> BorrowMut<[T]> for [T; N]
|
||||
```
|
||||
|
||||
##### Conflict
|
||||
|
||||
Now the question is why did it break the code… My first take was that the type
|
||||
`Rc<RefCell<T>>` has some _specialized_ implementation of the `.borrow_mut()` and
|
||||
the `use` overrides it with the default, which is true **in a sense**. However
|
||||
there is no _specialized_ implementation. Let's have a look at the trait and the
|
||||
type signature on the `RefCell<T>`:
|
||||
```rust
|
||||
// trait
|
||||
pub trait BorrowMut<Borrowed>: Borrow<Borrowed>
|
||||
where
|
||||
Borrowed: ?Sized,
|
||||
{
|
||||
fn borrow_mut(&mut self) -> &mut Borrowed;
|
||||
}
|
||||
|
||||
// ‹RefCell<T>.borrow_mut()› type signature
|
||||
pub fn borrow_mut(&self) -> RefMut<'_, T>
|
||||
```
|
||||
|
||||
I think that we can definitely agree on the fact that `RefMut<'_, T>` is not the
|
||||
`RefCell<T>`.
|
||||
|
||||
**In my opinion**, `RefCell<T>` implements a **separate** `.borrow_mut()` rather
|
||||
than implementing the interface, because it **cannot** satisfy the type requirements
|
||||
of the trait.
|
||||
|
||||
:::caution
|
||||
|
||||
I wonder how are we expected to deal with this conflict, if and when, we need
|
||||
both the `.borrow_mut()` of the trait and `.borrow_mut()` of the `RefCell<T>`.
|
||||
|
||||
:::
|
||||
|
||||
:::tip Fun fact
|
||||
|
||||
I was suggested by the compiler to do `use std::borrow::BorrowMut;` and break the
|
||||
code.
|
||||
|
||||
So much for the _almighty_ and _helpful_ compiler…
|
||||
|
||||
:::
|
||||
|
||||
## [Day 21: Monkey Math](https://adventofcode.com/2022/day/21)
|
||||
|
||||
:::info tl;dr
|
||||
|
||||
Computing an expression tree and then also finding ideal value for a node.
|
||||
|
||||
:::
|
||||
|
||||
### Solution
|
||||
|
||||
Relatively simple, until you get to the 2nd part where you start to practice
|
||||
a lot of the copy-paste. I have managed to sneak some perverted stuff in there
|
||||
though :) Let's go through the details.
|
||||
|
||||
#### `Default` trait
|
||||
|
||||
For the first time and twice I had a need to have a default value for my types,
|
||||
enumerations in this case. Rust offers a very nice trait[^4] that is described
|
||||
as:
|
||||
|
||||
> A trait for giving a type a useful default value.
|
||||
|
||||
I guess it sums it up nicely. The more interesting part about this is the fact
|
||||
that you can use the _macro machinery_ to save yourself some typing. If you have
|
||||
enumeration of which the default value doesn't bear any parameter, you can just
|
||||
do[^5]:
|
||||
|
||||
```rust
|
||||
#[derive(Default)]
|
||||
enum Color {
|
||||
#[default]
|
||||
White,
|
||||
Gray,
|
||||
Black,
|
||||
}
|
||||
```
|
||||
|
||||
#### Abusing negation
|
||||
|
||||
If you want to use a _unary minus_ operator on your own type, you can implement
|
||||
a `Neg` trait[^6]. I was dealing with a binary tree and needed a way how to look
|
||||
at the other side, so I have just implemented the negation for flipping between
|
||||
left and right :smile:
|
||||
|
||||
[^1]: Copy-pasting might be a relaxing thing to do, but you can also discover
|
||||
nasty stuff about your PC. See [this Reddit post and the comment].
|
||||
[^2]: [GitHub profile](https://github.com/martinjonas)
|
||||
[^3]: Even though you can use it even for libraries, but handling errors from
|
||||
libraries using `anyhow` is nasty… You will be the stinky one ;)
|
||||
[^4]: [`Default`](https://doc.rust-lang.org/std/default/trait.Default.html) docs
|
||||
[^5]: Pardon my example from the graph algorithms ;)
|
||||
[^6]: [`Neg`](https://doc.rust-lang.org/std/ops/trait.Neg.html) docs
|
||||
|
||||
[_Advent of Code_]: https://adventofcode.com
|
||||
[`itertools`]: https://crates.io/crates/itertools
|
||||
[this Reddit post and the comment]: https://www.reddit.com/r/adventofcode/comments/zb98pn/comment/iyq0ono
|
Loading…
Reference in a new issue