Add 2024 Quest 9

This commit is contained in:
Chris Alge 2024-11-15 11:55:50 +01:00
parent 30028df18f
commit 1c8def02c6
9 changed files with 416 additions and 0 deletions

View file

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

View file

@ -0,0 +1,55 @@
Part I
The sun has just set, so the city-dwellers are also heading for a well-deserved rest. You're just returning home when suddenly you notice a very bright shooting star. Except, this star isn't falling but... rising upwards! Upon reaching the highest point possible, the star splits into dozens, maybe even hundreds of smaller stars, each flying in a different direction until they vanish. After a while, another shooting star appears, followed by another, and another! You manage to pinpoint the origin. It's the Headquarters of Pyromancers near the market. You wonder where these bursts are coming from. Curiosity takes you to the source of the light.
The closer you get to the building, the brighter your surroundings become. It seems that mysterious objects are shining on the ground and are being launched into the sky by a trebuchet. You manage to squeeze through a crowd of onlookers and see a pyromancer in a slightly singed apron loading glowing spheres onto the trebuchet, which soon soar high into the air to burst into tiny sparks. What a spectacle!
The pyromancer is very eager to explain how it works. It turns out that the spheres are loaded with... beetles. Each beetle has numerous shiny dots made of a sparkling substance on its shell. When the sparkball (as the pyromancer calls it) reaches its highest point, it breaks apart, releasing the beetles, which, by rapidly fluttering their wings, scatter the glowing powder in all directions!
To achieve the desired brightness effect, you simply need to apply the right number of dots to the beetles, gather them into a sparkball, and launch them into the air. The pyromancer even has special stamps prepared that can apply several dots at once, but each beetle can be stamped only once.
There are 4 available stamps with the following numbers of dots: 1, 3, 5, 10.
It would be good to devise a way to stamp the beetles in such a way as to use as few of them as possible for the desired brightness in order to preserve some precious insects for future spectacles. Unfortunately, no one has come up with an efficient method yet, so the stamping process has become disorganized and inefficient. Of course, you step up to solve this problem!
The pyromancer hands you a list of what is needed to be prepared for the next showcase: (your notes).
Example based on the following notes:
2
4
7
16
Your input notes is a list of sparkballs, or to be more precise, a list of brightnesses you want to achieve. You have to calculate the minimum number of beetles you need to stamp for each ball and sum them up to get the total number of beetles needed for the whole list.
By looking at the example above, the optimal way to accomplish this requires:
2 beetles for constructing the first ball with brightness 2 (two 1-dot stamps)
2 beetles for brightness 4 (one 1-dot stamp and one 3-dot stamp)
3 beetles for brightness 7 (two 1-dot stamps and one 5-dot stamp)
3 beetles for brightness 16 (one 1-dot, one 5-dot, and one 10-dot stamp)
The minimum number of beetles you need for this sparkballs list is: 2 + 2 + 3 + 3 = 10.
What is the minimum number of beetles you need to stamp to construct all sparkballs from the list?
Part II
The next day, the tournament participants are invited to the Headquarters of the Pyromancers. It turns out that the task involves sparkballs and devising the most efficient method to utilize the beetles. It seems that the pyromancer you met the previous evening did not have time to inform his colleagues that you had already solved this problem. Well... Good for you!
The available stamps have changed to the following: 1, 3, 5, 10, 15, 16, 20, 24, 25, 30. Also, the list of sparkballs to be constructed is much longer now, but the core problem remains the same, so you feel very well prepared for this quest.
Example based on the following notes:
33
41
55
99
The optimal way to construct this sparkballs requires:
2 beetles for constructing the first ball with brightness 33 ( 30 + 3 )
2 beetles for brightness 41 ( 25 + 16 )
2 beetles for brightness 55 ( 30 + 25 )
4 beetles for brightness 99 ( 30 + 30 + 24 + 15 )
The minimum number of beetles you need for this sparkballs list is: 2 + 2 + 2 + 4 = 10.
What is the minimum number of beetles you need to stamp to construct all the sparkballs on the list?
Part III
In the evening, you encounter the same pyromancer again in the market, showcasing a light show, and you thank him for his assistance in the tournament. It turns out he has another problem to solve.
The sparkballs have caught the attention of the king himself, and he has placed an order for a display of gargantuan size! The brightnesses he wants are so high that there is no way to pack so many beetles in a single sphere, but the pyromancer has an idea. He wants to simply tie two sparkballs together and launch them into the air.
To achieve the target brightnesses from the king's list, each sparkball needs to be split into two spheres, but to keep the whole effect symmetric, they should have similar brightnesses. The maximum difference in brightness between such sparkball pairs is 100 dots. Any differences greater than this would ruin the final effect.
The pyromancer also constructed some additional stamps, so you can place many more dots on a single beetle! The full list of available stamps is now as follows: 1, 3, 5, 10, 15, 16, 20, 24, 25, 30, 37, 38, 49, 50, 74, 75, 100, 101.
Example based on the following notes:
156488
352486
546212
The best way to split the first brightness into two sparkballs is by using 775 beetles to create a sparkball with brightness 78275 , and another 775 beetles to create a sparkball with brightness 78213 , which sums up to the target brightness: 156488 . The brightness difference between those sparkballs is: 78275 - 78213 = 62, which is within the acceptable range.
The best way to split the second brightness is by using 1745 beetles for brightness of 176245 and another 1745 beetles for brightness of 176241 , which sums up to the target brightness: 352486 . The brightness difference between those sparkballs is: 176245 - 176241 = 4, which is within the acceptable range.
The best way to split the third brightness is by using 2705 beetles for brightness of 273156 and 2704 beetles for brightness of 273056 , which sums up to the target brightness: 546212 . The brightness difference between those sparkballs is: 273156 - 273056 = 100, which is at the edge of the acceptable range but still counted as valid.
The minimum number of beetles you need for this sparkballs list is 775 + 775 + 1745 + 1745 + 2705 + 2704 = 10449.
What is the minimum number of beetles you need to stamp to fulfil the king's order?

View file

@ -0,0 +1,134 @@
use std::collections::{BTreeSet, HashMap};
use std::mem;
use std::num::ParseIntError;
// This struct serves only to sort the open set in our A* algorithm.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Brightness {
expected: usize,
remaining: usize,
cost: usize,
last_step: usize,
}
// A* algorithm with memoization
fn required_stamps(brightness: usize, stamps: &[usize], mem: &mut HashMap<usize, usize>) -> usize {
let mut path = HashMap::new();
let mut open_set = BTreeSet::from([Brightness {
expected: brightness.div_ceil(*stamps.iter().find(|b| **b <= brightness).unwrap()),
remaining: brightness,
cost: 0,
last_step: 0,
}]);
while let Some(current) = open_set.pop_first() {
if current.remaining == 0 {
mem.insert(brightness, current.cost);
let mut prev_remaining = current.last_step;
let mut prev_cost = 1;
while prev_remaining < brightness {
if let Some(cost) = mem.get(&prev_remaining) {
prev_cost = *cost + 1;
} else {
mem.insert(prev_remaining, prev_cost);
prev_cost += 1;
}
prev_remaining = *path.get(&prev_remaining).unwrap();
}
return current.cost;
}
path.entry(current.remaining).or_insert_with(|| {
if let Some(cost) = mem.get(&current.remaining) {
open_set.insert(Brightness { expected: current.cost + *cost, remaining: 0, cost: current.cost + *cost, last_step: current.remaining, });
} else {
for stamp in stamps.iter().filter(|&s| *s <= current.remaining) {
let remaining = current.remaining - stamp;
let expected = current.cost + if remaining > 0 {
remaining.div_ceil(*stamps.iter().find(|b| **b <= remaining).unwrap())
} else { 1 };
open_set.insert(Brightness { expected, remaining, cost: current.cost + 1, last_step: *stamp, });
}
}
current.remaining + current.last_step
});
}
0
}
fn required_stamps_split(brightness: usize, stamps: &[usize], mem: &mut HashMap<usize, usize>) -> usize {
// Since the brightnesses must not differ by more than 100, we know that they must be
// `brightness/2+delta` and `brightness` minus that respectively, for some `delta` within [0..=50].
// We try all such pairs and return the lowest combined costs we find.
// Thanks to memoization, this becomes cheaper for later calls and is free for the second call
// at delta=0.
(0..=50).map(|delta|
required_stamps(brightness/2+delta, stamps, mem) +
required_stamps(brightness-(brightness/2+delta), stamps, mem)
).min().unwrap()
}
pub fn run(input: &str, part: usize) -> Result<usize, ParseIntError> {
let brightnesses: Vec<_> = input.lines().map(|n| n.parse()).collect::<Result<Vec<usize>, _>>()?;
match part {
1 => {
const STAMPS: [usize; 4] = [10, 5, 3, 1];
let mut mem = STAMPS.iter().map(|s| (*s, 1)).collect();
Ok(brightnesses.iter().map(|b| required_stamps(*b, &STAMPS, &mut mem)).sum())
},
2 => {
const STAMPS: [usize; 10] = [30, 25, 24, 20, 16, 15, 10, 5, 3, 1];
let mut mem = STAMPS.iter().map(|s| (*s, 1)).collect();
Ok(brightnesses.iter().map(|b| required_stamps(*b, &STAMPS, &mut mem)).sum())
},
3 => {
const STAMPS: [usize; 18] = [101, 100, 75, 74, 50, 49, 38, 37, 30, 25, 24, 20, 16, 15, 10, 5, 3, 1];
const ROUNDS: usize = 5;
let mut mem: HashMap<usize, usize> = STAMPS.iter().map(|s| (*s, 1)).collect();
// Precondition the memory to speed up the lookups a bit. This isn't worth the effort
// for the earlier parts, but here it saves a few seconds.
let mut last = STAMPS.to_vec();
last.reserve(STAMPS.len().pow(ROUNDS as u32 - 1));
let mut next = Vec::with_capacity(STAMPS.len().pow(ROUNDS as u32));
for round in 2..=ROUNDS+1 {
next.clear();
last.iter().for_each(|b| STAMPS.iter().for_each(|s| {
mem.entry(b+s).or_insert_with(|| {
next.push(b+s);
round
});
}));
mem::swap(&mut last, &mut next);
}
Ok(brightnesses.iter().map(|b| required_stamps_split(*b, &STAMPS, &mut mem)).sum())
},
_ => panic!("Illegal part number"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::read_to_string;
fn read_file(name: &str) -> String {
read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string()
}
#[test]
fn test_sample() {
let expected = [10, 10, 10449];
for part in 1..=expected.len() {
let sample_input = read_file(&format!("tests/sample{part}"));
assert_eq!(run(&sample_input, part), Ok(expected[part-1]));
}
}
#[test]
fn test_challenge() {
let expected = [13348, 5108, 150481];
for part in 1..=expected.len() {
let challenge_input = read_file(&format!("tests/challenge{part}"));
assert_eq!(run(&challenge_input, part), Ok(expected[part-1]));
}
}
}

View file

@ -0,0 +1,10 @@
16075
10000
12251
10059
17478
16287
13413
10107
17590
10099

View file

@ -0,0 +1,100 @@
1901
1489
1514
1503
1029
1905
1170
1236
1216
1001
1858
1046
1244
1517
1389
1899
1746
1483
1844
1243
1518
1853
1068
1261
1270
1255
1997
1273
1073
1107
1060
1373
1361
1219
1667
1837
1507
1684
1174
1807
1161
1440
1490
1678
1699
1736
1641
1559
1543
1692
1500
1043
1096
1877
1333
1410
1982
1433
1753
1168
1940
1992
1805
1887
1112
1390
1518
1672
1732
1427
1782
1746
1756
1588
1106
1278
1250
1446
1021
1571
1790
1861
1090
1361
1923
1610
1663
1737
1962
1135
1806
1933
1673
1186
1975
1499
1269
1291
1402
1955

View file

@ -0,0 +1,100 @@
105465
120198
179472
184734
163769
196703
147938
180556
124241
161073
143406
129525
149468
144770
133477
134059
194774
170166
161596
122353
115189
130711
150790
138153
166441
121275
125526
180468
169375
134447
117517
131156
188386
149955
140607
163147
180770
150938
173507
105294
154266
123139
144843
173495
133269
162199
192950
132921
166215
156277
190642
166142
137360
120062
183617
156481
192708
147761
162391
150856
110702
158267
196368
102725
165434
122481
102027
157437
183448
160137
129147
197999
119792
191435
181830
189017
129330
160070
193568
132462
197362
169412
116447
139995
183853
191320
186982
123023
194347
104713
124902
133735
169331
110762
140378
170936
152397
105219
126582
139449

View file

@ -0,0 +1,4 @@
2
4
7
16

View file

@ -0,0 +1,4 @@
33
41
55
99

View file

@ -0,0 +1,3 @@
156488
352486
546212