diff --git a/2021/day14/day14_rs/Cargo.toml b/2021/day14/day14_rs/Cargo.toml
new file mode 100644
index 0000000..0984fea
--- /dev/null
+++ b/2021/day14/day14_rs/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "day14_rs"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+nom = "7.1.0"
diff --git a/2021/day14/day14_rs/src/main.rs b/2021/day14/day14_rs/src/main.rs
new file mode 100644
index 0000000..77fef7e
--- /dev/null
+++ b/2021/day14/day14_rs/src/main.rs
@@ -0,0 +1,152 @@
+#![warn(clippy::pedantic)]
+use nom::{
+ bytes::complete::{tag, take_till},
+ character::complete::{anychar, newline},
+ combinator::recognize,
+ multi::many1,
+ sequence::{pair, separated_pair, terminated},
+};
+use std::collections::BTreeMap;
+use std::io::{stdin, Read};
+use std::ops::Add;
+
+type Input<'a> = &'a str;
+type IResult<'a, T> = nom::IResult, T>;
+
+type Rule = ((char, char), char);
+
+fn rule(i: Input) -> IResult {
+ separated_pair(pair(anychar, anychar), tag(" -> "), anychar)(i)
+}
+
+fn parse_input(i: Input) -> IResult<(&str, Vec)> {
+ separated_pair(
+ terminated(recognize(take_till(|c| c == '\n')), newline),
+ newline,
+ many1(terminated(rule, newline)),
+ )(i)
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct Counter {
+ counts: BTreeMap,
+}
+
+impl Counter {
+ pub fn new() -> Self {
+ Self {
+ counts: BTreeMap::new(),
+ }
+ }
+
+ pub fn push(&mut self, el: T) {
+ *self.counts.entry(el).or_insert(0) += 1;
+ }
+
+ pub fn most_common(&self) -> Option<(&T, usize)> {
+ self.counts
+ .iter()
+ .map(|(el, count)| (el, *count))
+ .max_by_key(|&(_, count)| count)
+ }
+
+ pub fn least_common(&self) -> Option<(&T, usize)> {
+ self.counts
+ .iter()
+ .map(|(el, count)| (el, *count))
+ .min_by_key(|&(_, count)| count)
+ }
+}
+
+impl FromIterator for Counter {
+ fn from_iter>(iter: I) -> Self {
+ let mut counts = Self::new();
+ for el in iter {
+ counts.push(el);
+ }
+ counts
+ }
+}
+
+impl Add<&Counter> for Counter {
+ type Output = Self;
+
+ fn add(mut self, other: &Self) -> Self {
+ for (el, count) in &other.counts {
+ *self.counts.entry(*el).or_insert(0) += count;
+ }
+ self
+ }
+}
+
+impl Add for Counter {
+ type Output = Self;
+
+ fn add(mut self, other: Self) -> Self {
+ for (el, count) in other.counts {
+ *self.counts.entry(el).or_insert(0) += count;
+ }
+ self
+ }
+}
+
+#[derive(Clone, Debug)]
+struct LetterCounter {
+ rules: BTreeMap<(char, char), char>,
+ counts: BTreeMap<(char, char, usize), Counter>,
+}
+
+impl LetterCounter {
+ pub fn new(rules: BTreeMap<(char, char), char>) -> Self {
+ let mut counts = BTreeMap::new();
+ for &(left, right) in rules.keys() {
+ counts.insert((left, right, 0), Counter::from_iter([left]));
+ }
+ Self { rules, counts }
+ }
+
+ pub fn get_counts_right_exclusive(
+ &mut self,
+ left: char,
+ right: char,
+ depth: usize,
+ ) -> &Counter {
+ #[allow(clippy::map_entry)] // lifetimes don't work out
+ if !self.counts.contains_key(&(left, right, depth)) {
+ let middle = self.rules[&(left, right)];
+ let counts_left = self
+ .get_counts_right_exclusive(left, middle, depth - 1)
+ .clone();
+ let counts_right = self.get_counts_right_exclusive(middle, right, depth - 1);
+ let counts = counts_left + counts_right;
+ self.counts.insert((left, right, depth), counts);
+ }
+ &self.counts[&(left, right, depth)]
+ }
+}
+
+fn main() {
+ let mut input = String::new();
+ stdin().lock().read_to_string(&mut input).unwrap();
+
+ let (input, rules) = parse_input(&input).unwrap().1;
+
+ let rules: BTreeMap<_, _> = rules.into_iter().collect();
+ let chars: Vec<_> = input.chars().collect();
+
+ let mut counter = LetterCounter::new(rules);
+
+ let mut run = |steps| {
+ let mut totals = chars.windows(2).fold(Counter::new(), |counts, x| {
+ counts + counter.get_counts_right_exclusive(x[0], x[1], steps)
+ });
+ totals.push(*chars.last().unwrap());
+
+ println!(
+ "{:?}",
+ totals.most_common().unwrap().1 - totals.least_common().unwrap().1
+ );
+ };
+ run(10);
+ run(40);
+}