feat(dosages): added library function to calculate optimal combination of medicines for desired dosage
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
/target
|
||||
|
||||
|
||||
# Added by cargo
|
||||
#
|
||||
# already existing elements were commented out
|
||||
|
||||
#/target
|
||||
Generated
+7
@@ -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"
|
||||
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "lsdp"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,137 @@
|
||||
use std::{cmp::max, collections::HashMap, ops::Index};
|
||||
|
||||
pub struct ComboNode {
|
||||
combo: Vec<u32>,
|
||||
weight: u32,
|
||||
}
|
||||
|
||||
impl Index<usize> 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<u32>) -> Self {
|
||||
let sum = combo.iter().sum();
|
||||
Self { combo, weight: sum }
|
||||
}
|
||||
|
||||
pub fn get_neighbours(&self) -> impl Iterator<Item = ComboNode> {
|
||||
(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<u32, ComboNode>,
|
||||
values: Vec<u32>,
|
||||
target: u32,
|
||||
queue: Vec<ComboNode>,
|
||||
}
|
||||
|
||||
impl ComboGraph {
|
||||
pub fn new(mut values: Vec<u32>, 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<Vec<u32>> {
|
||||
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<Vec<u32>> {
|
||||
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<u32>, target: u32, rounded: bool) -> Vec<u32> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
+101
@@ -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>) -> 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>) -> 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]);
|
||||
}
|
||||
}
|
||||
+71
@@ -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<u32>) -> Vec<u32> {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user