mirror of
https://github.com/bertptrs/adventofcode.git
synced 2025-12-25 21:00:31 +01:00
Slightly more efficient search
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
use std::array;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::ops::Add;
|
||||
use std::ops::Sub;
|
||||
|
||||
use anyhow::Result;
|
||||
use nom::bytes::complete::tag;
|
||||
@@ -45,81 +44,146 @@ impl TryFrom<&'_ [u8]> for Mineral {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
struct Resources([u8; 4]);
|
||||
|
||||
impl Resources {
|
||||
fn enough_for(self, other: Self) -> bool {
|
||||
self.0.iter().zip(&other.0).all(|(a, b)| a >= b)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Resources {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self(std::array::from_fn(|i| self.0[i] - rhs.0[i]))
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<[u8; 4]> for Resources {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: [u8; 4]) -> Self::Output {
|
||||
Self(std::array::from_fn(|i| self.0[i] + rhs[i]))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BluePrint {
|
||||
id: u32,
|
||||
costs: [Resources; 4],
|
||||
costs: [[u8; 3]; 4],
|
||||
}
|
||||
|
||||
impl BluePrint {
|
||||
pub fn max_geodes(&self, time: u32) -> u32 {
|
||||
self.max_geodes_recursive(time, 0, [1, 0, 0, 0], Resources::default()) as u32
|
||||
}
|
||||
|
||||
fn max_geodes_recursive(
|
||||
&self,
|
||||
time_left: u32,
|
||||
// forbidden is a bitset for convenience
|
||||
forbidden: u8,
|
||||
machines: [u8; 4],
|
||||
resources: Resources,
|
||||
) -> u8 {
|
||||
if time_left <= 1 || forbidden.count_ones() == 4 {
|
||||
return resources.0[3] + machines[3] * (time_left as u8);
|
||||
}
|
||||
|
||||
let resources_after = resources + machines;
|
||||
|
||||
let mut best = 0;
|
||||
|
||||
let mut can_buy = 0;
|
||||
|
||||
for (i, &cost) in self.costs.iter().enumerate() {
|
||||
if ((1 << i) & forbidden) == 0 && resources.enough_for(cost) {
|
||||
can_buy |= 1 << i;
|
||||
let mut new_machines = machines;
|
||||
new_machines[i] += 1;
|
||||
|
||||
best = best.max(self.max_geodes_recursive(
|
||||
time_left - 1,
|
||||
0,
|
||||
new_machines,
|
||||
resources_after - cost,
|
||||
))
|
||||
pub fn max_geodes(&self, time: u8) -> u8 {
|
||||
/// How much would we produce if all we did was produce geode robots for the remaining time
|
||||
fn ideal(remaining: u32) -> u32 {
|
||||
if remaining <= 1 {
|
||||
0
|
||||
} else {
|
||||
(remaining - 1) * remaining / 2
|
||||
}
|
||||
}
|
||||
|
||||
best.max(self.max_geodes_recursive(
|
||||
time_left - 1,
|
||||
forbidden | can_buy,
|
||||
#[derive(Eq, PartialEq)]
|
||||
struct State {
|
||||
missed: u32,
|
||||
got: u8,
|
||||
time_left: u8,
|
||||
resources: [u8; 3],
|
||||
machines: [u8; 3],
|
||||
}
|
||||
|
||||
impl Ord for State {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
Ordering::Equal
|
||||
.then(other.missed.cmp(&self.missed))
|
||||
.then(self.got.cmp(&other.got))
|
||||
.then(self.time_left.cmp(&other.time_left))
|
||||
.then(self.machines.cmp(&other.machines))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for State {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
let max_needed = self.max_needed();
|
||||
let mut todo = BinaryHeap::new();
|
||||
let mut best = 0;
|
||||
|
||||
todo.push(State {
|
||||
missed: 0,
|
||||
got: 0,
|
||||
time_left: time,
|
||||
resources: [0; 3],
|
||||
machines: [1, 0, 0],
|
||||
});
|
||||
|
||||
while let Some(State {
|
||||
missed,
|
||||
got,
|
||||
time_left,
|
||||
resources,
|
||||
machines,
|
||||
resources_after,
|
||||
))
|
||||
}) = todo.pop()
|
||||
{
|
||||
let ideal_from_now = ideal(time_left as u32);
|
||||
if u32::from(best - got) > ideal_from_now {
|
||||
continue;
|
||||
}
|
||||
if todo.len() > 1_000_000 {
|
||||
panic!(
|
||||
"Safety: got a todo list of len {}, best: {best}",
|
||||
todo.len()
|
||||
);
|
||||
}
|
||||
'element: for element in 0..4 {
|
||||
let mut min_to_build = 0;
|
||||
for ((&cost, &avail), &machine) in
|
||||
self.costs[element].iter().zip(&resources).zip(&machines)
|
||||
{
|
||||
if cost > avail {
|
||||
if machine == 0 {
|
||||
continue 'element;
|
||||
} else {
|
||||
min_to_build = min_to_build.max((cost - avail + machine - 1) / machine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +1 because we need a turn to build
|
||||
let built_after = min_to_build + 1;
|
||||
if built_after >= time_left {
|
||||
continue;
|
||||
}
|
||||
|
||||
let resources_after = array::from_fn(|i| {
|
||||
resources[i] + machines[i] * built_after - self.costs[element][i]
|
||||
});
|
||||
let time_after = time_left - built_after;
|
||||
|
||||
if element == Mineral::Geode as usize {
|
||||
let new_got = got + time_after;
|
||||
todo.push(State {
|
||||
missed,
|
||||
got: new_got,
|
||||
time_left: time_after,
|
||||
resources: resources_after,
|
||||
machines,
|
||||
});
|
||||
|
||||
best = best.max(new_got);
|
||||
} else {
|
||||
if machines[element] >= max_needed[element] {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut new_machines = machines;
|
||||
new_machines[element] += 1;
|
||||
let new_missed = ideal_from_now - ideal(time_after as u32);
|
||||
todo.push(State {
|
||||
missed: new_missed,
|
||||
got,
|
||||
time_left: time_after,
|
||||
resources: resources_after,
|
||||
machines: new_machines,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best
|
||||
}
|
||||
|
||||
fn max_needed(&self) -> [u8; 3] {
|
||||
let mut max_needed = [0; 3];
|
||||
|
||||
for cost in &self.costs {
|
||||
for (max, &new) in max_needed.iter_mut().zip(cost) {
|
||||
*max = (*max).max(new);
|
||||
}
|
||||
}
|
||||
|
||||
max_needed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +201,7 @@ fn parse_blueprint(input: &[u8]) -> IResult<&[u8], BluePrint> {
|
||||
let (mut input, id) =
|
||||
terminated(delimited(tag("Blueprint "), u32, tag(":")), multispace1)(input)?;
|
||||
|
||||
let mut costs: [Resources; 4] = Default::default();
|
||||
let mut costs: [[u8; 3]; 4] = Default::default();
|
||||
|
||||
let mut parse_robot = terminated(
|
||||
tuple((
|
||||
@@ -152,10 +216,10 @@ fn parse_blueprint(input: &[u8]) -> IResult<&[u8], BluePrint> {
|
||||
let (remaining, (element, (amount1, req1), cost2)) = parse_robot(input)?;
|
||||
input = remaining;
|
||||
|
||||
costs[element as usize].0[req1 as usize] = amount1;
|
||||
costs[element as usize][req1 as usize] = amount1;
|
||||
|
||||
if let Some((amount2, req2)) = cost2 {
|
||||
costs[element as usize].0[req2 as usize] = amount2;
|
||||
costs[element as usize][req2 as usize] = amount2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +254,25 @@ mod tests {
|
||||
|
||||
const SAMPLE: &[u8] = include_bytes!("./samples/19.txt");
|
||||
|
||||
fn get_samples() -> Vec<BluePrint> {
|
||||
parse_input(SAMPLE, many1(parse_blueprint)).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_part1() {
|
||||
let samples = get_samples();
|
||||
|
||||
assert_eq!(samples[0].max_geodes(24), 9);
|
||||
assert_eq!(samples[1].max_geodes(24), 12);
|
||||
|
||||
assert_eq!(part1(SAMPLE).unwrap(), "33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_part2() {
|
||||
let samples = get_samples();
|
||||
|
||||
assert_eq!(samples[0].max_geodes(32), 56);
|
||||
assert_eq!(samples[1].max_geodes(32), 62);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user