From 7abbc37a65122a5e0bb8e78679b00e5e59175b67 Mon Sep 17 00:00:00 2001 From: Markus Brueckner Date: Thu, 5 Dec 2024 21:54:39 +0100 Subject: [PATCH] - Day 5 --- 2024/day5/Cargo.lock | 7 +++ 2024/day5/Cargo.toml | 6 +++ 2024/day5/README.md | 37 +++++++++++++ 2024/day5/src/main.rs | 121 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+) create mode 100644 2024/day5/Cargo.lock create mode 100644 2024/day5/Cargo.toml create mode 100644 2024/day5/README.md create mode 100644 2024/day5/src/main.rs diff --git a/2024/day5/Cargo.lock b/2024/day5/Cargo.lock new file mode 100644 index 0000000..7a62bb0 --- /dev/null +++ b/2024/day5/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day5" +version = "0.1.0" diff --git a/2024/day5/Cargo.toml b/2024/day5/Cargo.toml new file mode 100644 index 0000000..49bdb49 --- /dev/null +++ b/2024/day5/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "day5" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/2024/day5/README.md b/2024/day5/README.md new file mode 100644 index 0000000..0b172a2 --- /dev/null +++ b/2024/day5/README.md @@ -0,0 +1,37 @@ +# Solution Day 5 + +Slightly more complex file format this time. It consists of two parts: "rules" or the form `x|y` and "updates", that look like `x,y,z,...`. +Luckily for parsing we can just go through the `.lines()` iterator one by one, check whether the line contains a `|` and, if so, parse +it into a rule struct. If not, we just continue splitting it into `Vec`. We can do this the simple way consuming each line to look +for the `|`, because there's an empty line between the blocks, that we can just throw away. + +After everything is parsed, the "real" work starts: writing a rule-interpreter, that checks, whether the updates are correct by the rules. +The solution is rather simple: for each element we check, whether any element, that should come _after_ it, actually comes before and the +other way around. If this isn't the case, the update is correct. + +While the rule-checker is reasonably simple, it _does_ need to go through all rules twice for each element of the update. This could be +improved by indexing the rules in a `HashMap`based on their constituents and do an `O(1)` lookup. The runtime is so fast, however, that +this is not worth the effort. + +## Task 1 + +This task is easy: find all correct updates (just a filter with the rule interpreter) and sum up the middle elements. Since all updates are +of odd length, the middle element always exists, so `.reduce()` can do quick work. + +## Task 2 + +Slightly more complex: the *in*correct rules should be retrieved and fixed, before the middle element is supposed to be summed up. Took +me a while, but then I had a bright idea: the rules all together are nothing more than an ordering. If you interpret all rules against a +pair of update entries, you can tell which way around they are supposed to go, i.e. which one is less than the other. So we can use +the `.sort_by()` function from the standard library with a custom comparison function, that interprets all relevant rules and returns the +ordering. After sorting the elements, summing up the middle is the same as in task 1. + +_Note:_ This assumes, that the full set of rules represents a total order. If it were to be a partial order, the resulting order would be +unspecified. Luckily for us, the designers made the ruleset a total order. + +This could again be more efficient, by indexing the rules once first and then do an `O(1)` lookup for all relevant rules instead of checking +_all_ rules for each call to the comparator. Again, the runtime is so quick, that this seems unnecessary optimization. + +Also, there's a `.clone()` for the update vector in there, because I couldn't figure out how to tell the borrow checker, that I would be +happy to fully consume the input while checking it. There's no need for me to touch any update twice, so borrowing isn't actually necessary. +I just didn't find the right combination of mutable vectors and `IntoIterator`s to make this clear to the borrow checker. \ No newline at end of file diff --git a/2024/day5/src/main.rs b/2024/day5/src/main.rs new file mode 100644 index 0000000..1d9786f --- /dev/null +++ b/2024/day5/src/main.rs @@ -0,0 +1,121 @@ +use std::cmp::Ordering; + +struct PageOrderingRule { + first: u32, + second: u32, +} + +impl From<&str> for PageOrderingRule { + fn from(s: &str) -> Self { + let mut parts = s.split("|"); + Self { + first: parts + .next() + .expect("Missing first rule part") + .parse() + .expect("Cannot parse first rule part"), + second: parts + .next() + .expect("Missing second rule part") + .parse() + .expect("Cannot parse second rule part"), + } + } +} + +fn is_correct(update: &Vec, rules: &Vec) -> bool { + for idx in 0..update.len() { + let entry = update[idx]; + let before_rules = rules.iter().filter(|rule| rule.first == entry); + let after_rules = rules.iter().filter(|rule| rule.second == entry); + + // none of the elements before the checked one should come after it + let before_correct = before_rules.clone().all(|rule| { + update[..idx] + .iter() + .all(|&candidate| candidate != rule.second) + }); + // none of the elements after the checked one should actually come before it + let after_correct = after_rules.clone().all(|rule| { + update[idx..] + .iter() + .all(|&candidate| candidate != rule.first) + }); + if !before_correct || !after_correct { + return false; + } + } + true +} + +fn load_input() -> (Vec, Vec>) { + let data = std::fs::read_to_string("./input.txt").expect("Cannot read file"); + let mut rules: Vec = vec![]; + let mut lines = data.lines(); + + loop { + match lines.next() { + None => break, + Some(text) => { + if text.contains("|") { + rules.push(text.into()); + } else { + break; // no longer a rule. We've moved to the next part of the file + } + } + } + } + + let updates: Vec> = lines + .map(|line| { + line.split(",") + .map(|entry| entry.parse().expect("Cannot parse entry")) + .collect() + }) + .collect(); + + (rules, updates) +} + +fn task1() { + let (rules, updates) = load_input(); + let correct_updates = updates.iter().filter(|update| is_correct(&update, &rules)); + let sum = correct_updates.fold(0, |existing, update| existing + update[update.len() / 2]); + + println!("Task 1: Sum: {sum}"); +} + +fn task2() { + let (rules, updates) = load_input(); + let incorrect_updates = updates + .iter() + .filter(|&update| !is_correct(&update, &rules)); + + let corrected_updates = incorrect_updates.into_iter().map(|update| { + let mut copy = update.clone(); // TODO don't like the clone here, but sort_by requires a mutable reference + copy.sort_by(|a, b| { + // basically evaluate every rule to sort the thing + for rule in rules.iter() { + if rule.first == *a && rule.second == *b { + // correct order + return Ordering::Less; + } + if rule.first == *b && rule.second == *a { + // wrong way around + return Ordering::Greater; + } + } + Ordering::Equal + }); + copy + }); + + let sum = corrected_updates.fold(0, |existing, update| existing + update[update.len() / 2]); + + println!("Task 2: Sum: {sum}"); +} + +fn main() { + task1(); + task2(); +}