This commit is contained in:
Markus Brueckner 2024-12-05 21:54:39 +01:00
parent 22168572c8
commit 7abbc37a65
4 changed files with 171 additions and 0 deletions

7
2024/day5/Cargo.lock generated Normal file
View file

@ -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"

6
2024/day5/Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[package]
name = "day5"
version = "0.1.0"
edition = "2021"
[dependencies]

37
2024/day5/README.md Normal file
View file

@ -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<u32>`. 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.

121
2024/day5/src/main.rs Normal file
View file

@ -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<u32>, rules: &Vec<PageOrderingRule>) -> 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<PageOrderingRule>, Vec<Vec<u32>>) {
let data = std::fs::read_to_string("./input.txt").expect("Cannot read file");
let mut rules: Vec<PageOrderingRule> = 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<Vec<u32>> = 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();
}