commit e1d47d6a28dd0b835f20ca63bfb969a7de955568feb8dd6e8caa6c32736fb34b Author: professorwaltwood Date: Sun Aug 2 17:47:53 2026 +1000 feat(dosages): added library function to calculate optimal combination of medicines for desired dosage diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b676c71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target + + +# Added by cargo +# +# already existing elements were commented out + +#/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..28fba96 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "lsdp" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dd6e720 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "lsdp" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/src/combo_graph.rs b/src/combo_graph.rs new file mode 100644 index 0000000..d57f39e --- /dev/null +++ b/src/combo_graph.rs @@ -0,0 +1,137 @@ +use std::{cmp::max, collections::HashMap, ops::Index}; + +pub struct ComboNode { + combo: Vec, + weight: u32, +} + +impl Index for ComboNode { + type Output = u32; + fn index(&self, index: usize) -> &Self::Output { + self.combo.index(index) + } +} + +impl ComboNode { + pub fn weight(&self) -> u32 { + self.weight + } + + pub fn new(combo: Vec) -> Self { + let sum = combo.iter().sum(); + Self { combo, weight: sum } + } + + pub fn get_neighbours(&self) -> impl Iterator { + (0..self.combo.len()).map(|i| { + let mut combo = self.combo.clone(); + combo[i] += 1; + Self { + combo, + weight: self.weight + 1, + } + }) + } +} + +pub struct ComboGraph { + nodes: HashMap, + values: Vec, + target: u32, + queue: Vec, +} + +impl ComboGraph { + pub fn new(mut values: Vec, target: u32) -> Self { + // ensure values is sorted + values.sort_unstable(); + let num_values = values.len(); + Self { + nodes: HashMap::new(), + values, + target, + queue: vec![ComboNode::new(vec![0; num_values])], + } + } + + pub fn closest_node_target(&self) -> Option<&ComboNode> { + let closest = self + .nodes + .iter() + // Treat a zero difference as a difference of 1 to accomodate for rounding + .map(|(pos, combo)| (max(self.target.abs_diff(*pos), 1), combo.weight(), pos)) + .min()?; + + self.nodes.get(&closest.2) + } + + /// Visit a combo and calculate its position and weight + pub fn visit(&mut self, node: ComboNode) -> bool { + let mut pos = 0; + + for i in 0..self.values.len() { + pos += self.values[i] * node[i]; + } + + // Check if pos already found with less weight + let current = self.nodes.get(&pos); + if let Some(combo) = current { + if combo.weight() > node.weight() { + // should never happen + panic!("Found better path second"); + } + return false; + } else if pos == self.target { + self.nodes.insert(pos, node); + return true; + } else if pos < self.target { + node.get_neighbours().for_each(|n| { + self.queue.push(n); + }); + } + self.nodes.insert(pos, node); + + false + } + + /// grows the graph to find the optimal path + pub fn grow(&mut self) -> bool { + let mut next = self.queue.pop(); + while let Some(node) = next { + if self.visit(node) { + return true; + } + next = self.queue.pop(); + } + false + } + + pub fn target_combo(&self) -> Option> { + let combo = self.nodes.get(&self.target)?; + Some( + (0..self.values.len()) + .map(|i| vec![self.values[i]; combo[i] as usize]) + .flatten() + .collect(), + ) + } + + pub fn closest_combo(&self) -> Option> { + let combo = self.closest_node_target()?; + Some( + (0..self.values.len()) + .map(|i| vec![self.values[i]; combo[i] as usize]) + .flatten() + .collect(), + ) + } +} + +pub fn find_optimal_combo(strengths: Vec, target: u32, rounded: bool) -> Vec { + let mut graph = ComboGraph::new(strengths, target); + if graph.grow() && !rounded { + graph.target_combo().expect("grow signalled target hit") + } else { + graph.closest_combo().expect("non empty graph") + } +} diff --git a/src/factor.rs b/src/factor.rs new file mode 100644 index 0000000..3a8ca4c --- /dev/null +++ b/src/factor.rs @@ -0,0 +1,101 @@ +/// calculate greatest common factor +/// requires a > b +fn gcf(mut a: u32, mut b: u32) -> u32 { + let mut rem = a % b; + + while rem != 0 { + a = b; + b = rem; + rem = a % b; + } + b +} + +/// Calculate gcf of vector +fn gcfv(v: &Vec) -> u32 { + let mut i = 1; + let mut cgcf = v[0]; // start with first item + let mut a; + let mut b; + while i < v.len() { + if cgcf >= v[i] { + a = cgcf; + b = v[i]; + } else { + a = v[i]; + b = cgcf; + } + cgcf = gcf(a, b); + + i += 1; + } + + cgcf +} + +pub fn factorise(strengths: &mut Vec) -> u32 { + let factor = gcfv(strengths); + if factor == 1 { + return factor; + } + for s in strengths.iter_mut() { + *s = *s / factor; + } + factor +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gcf_base_case() { + assert_eq!(gcf(1, 1), 1); + } + + #[test] + fn gcf_standard_case() { + assert_eq!(gcf(1071, 462), 21) + } + + #[test] + fn gcf_coprime_case() { + assert_eq!(gcf(1071, 461), 1) + } + + #[test] + fn gcfv_base_case() { + assert_eq!(gcfv(&vec![1]), 1); + } + + #[test] + fn gcfv_standard_case() { + assert_eq!(gcfv(&vec![1071, 462, 42, 105]), 21) + } + + #[test] + fn gcfv_coprime_case() { + assert_eq!(gcfv(&vec![1071, 105, 42, 461]), 1) + } + + #[test] + fn factorise_base_case() { + let mut s = vec![1]; + assert_eq!(factorise(&mut s), 1); + assert_eq!(s, vec![1]); + } + + #[test] + fn factorise_standard_case() { + let mut s = vec![1071, 462, 42, 105]; + assert_eq!(factorise(&mut s), 21); + assert_eq!(s, vec![51, 22, 2, 5]); + } + + #[test] + fn factorise_coprime_case() { + let mut s = vec![1071, 105, 42, 461]; + assert_eq!(factorise(&mut s), 1); + assert_eq!(s, vec![1071, 105, 42, 461]); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..859c3fa --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,71 @@ +mod combo_graph; +mod factor; + +use crate::combo_graph::find_optimal_combo; +use crate::factor::factorise; + +/// Algorithm: +/// 1. factorise strengths by GCF +/// 2. Perform DFS search on strength combos starting from full largest strength +/// 3. use efficiencies from dynamic programming to prune cases already calculated +/// +/// ``` +/// use lsdp::calculate_combo; +/// let combo = calculate_combo(173, vec![20, 57]); +/// assert_eq!(combo, vec![20, 20, 20, 57, 57]); +/// ``` +/// +pub fn calculate_combo(mut target: u32, mut strengths: Vec) -> Vec { + if strengths.len() == 0 { + return vec![]; + } + + let factor = factorise(&mut strengths); + let mut rounded = false; + + if factor != 1 { + if target % factor != 0 { + let f_target: f64 = target as f64 / factor as f64; + target = f_target.round() as u32; + rounded = true; + } else { + target /= factor; + rounded = false; + } + } + + let combo = find_optimal_combo(strengths, target, rounded); + + combo.iter().map(|s| s * factor).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn base_case() { + assert_eq!(calculate_combo(1, vec![1]), vec![1]); + } + + #[test] + fn test_basic() { + assert_eq!( + calculate_combo(245, vec![50, 20, 5]), + vec![5, 20, 20, 50, 50, 50, 50] + ); + } + + #[test] + fn test_basic_close() { + assert_eq!( + calculate_combo(245, vec![50, 20, 4]), + vec![4, 20, 20, 50, 50, 50, 50] + ); + } + + #[test] + fn test_no_exact() { + assert_eq!(calculate_combo(173, vec![20, 57]), vec![20, 20, 20, 57, 57]); + } +}