use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Inputs {
pub round: u32,
pub size_ratio: u32,
pub arrival_secs: f64,
pub merge_secs: f64,
pub merge_query_share: f64,
pub scan_share: f64,
pub false_positive_rate: f64,
}
#[expect(
clippy::suboptimal_flops,
reason = "`mul_add` needs std, and the plan is solved without it too"
)]
impl Inputs {
#[must_use]
pub fn speed(&self, runs: u32) -> f64 {
let r = self.scan_share;
1.0 / ((f64::from(runs) + 2.0) * r + (1.0 - r) * (1.0 + self.false_positive_rate))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Plan {
merge_at: Vec<u32>,
}
impl Plan {
#[must_use]
pub fn merge_after(&self, units: u32) -> u32 {
self.merge_at
.get(usize::try_from(units).unwrap_or(usize::MAX))
.copied()
.unwrap_or(1)
}
#[must_use]
pub fn solve(inputs: &Inputs) -> Self {
let round = inputs.round.clamp(1, MAX_ROUND);
let last = inputs.size_ratio.min(MAX_SIZE_RATIO) * round;
let mut solver = Solver {
inputs,
memo: crate::HashMap::default(),
};
let mut merge_at = alloc::vec![1; usize::try_from(round).unwrap_or(0) + 1];
solver.schedule(0, round, last, 0, &mut merge_at);
Self { merge_at }
}
}
pub const MAX_ROUND: u32 = 64;
pub const MAX_SIZE_RATIO: u32 = 64;
struct Solver<'a> {
inputs: &'a Inputs,
memo: crate::HashMap<(u32, u32, u32), (f64, u32)>,
}
#[expect(
clippy::suboptimal_flops,
reason = "`mul_add` needs std, and the plan is solved without it too"
)]
impl Solver<'_> {
fn best(&mut self, e: u32, c: u32, m: u32) -> f64 {
if let Some(&(score, _)) = self.memo.get(&(e, c, m)) {
return score;
}
let (tw, tc) = (self.inputs.arrival_secs, self.inputs.merge_secs);
let solved = if c <= 1 {
let units = f64::from(m) + f64::from(c);
(
units * tc * self.inputs.merge_query_share * self.inputs.speed(e),
1,
)
} else {
let mut best = (
tw * self.inputs.speed(e + 1) + self.best(e + 1, c - 1, m + 1),
1,
);
for x in 2..c {
let score = self.best(e, x, 0)
+ (tw - f64::from(x) * tc) * self.inputs.speed(e + 1)
+ self.best(e + 1, c - x, m + x);
if score > best.0 {
best = (score, x);
}
}
best
};
self.memo.insert((e, c, m), solved);
solved.0
}
fn schedule(&mut self, e: u32, c: u32, m: u32, start: u32, merge_at: &mut [u32]) {
if c <= 1 {
return;
}
self.best(e, c, m);
let x = self.memo.get(&(e, c, m)).map_or(1, |&(_, x)| x);
if x > 1 {
self.schedule(e, x, 0, start, merge_at);
if let Some(slot) = merge_at.get_mut(usize::try_from(start + x).unwrap_or(usize::MAX)) {
*slot = (*slot).max(x);
}
}
self.schedule(e + 1, c - x, m + x, start + x, merge_at);
}
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "test code")]
mod tests;