Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Minimum Cost Path #842

Merged
merged 2 commits into from
Nov 13, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 156 additions & 59 deletions src/dynamic_programming/minimum_cost_path.rs
Original file line number Diff line number Diff line change
@@ -1,80 +1,177 @@
/// Minimum Cost Path via Dynamic Programming

/// Find the minimum cost traced by all possible paths from top left to bottom right in
/// a given matrix, by allowing only right and down movement

/// For example, in matrix,
/// [2, 1, 4]
/// [2, 1, 3]
/// [3, 2, 1]
/// The minimum cost path is 7

/// # Arguments:
/// * `matrix` - The input matrix.
/// # Complexity
/// - time complexity: O( rows * columns ),
/// - space complexity: O( rows * columns )
use std::cmp::min;

pub fn minimum_cost_path(mut matrix: Vec<Vec<usize>>) -> usize {
// Add rows and columns variables for better readability
let rows = matrix.len();
let columns = matrix[0].len();
/// Represents possible errors that can occur when calculating the minimum cost path in a matrix.
#[derive(Debug, PartialEq, Eq)]
pub enum MatrixError {
/// Error indicating that the matrix is empty or has empty rows.
EmptyMatrix,
/// Error indicating that the matrix is not rectangular in shape.
NonRectangularMatrix,
}

// Preprocessing the first row
for i in 1..columns {
matrix[0][i] += matrix[0][i - 1];
/// Computes the minimum cost path from the top-left to the bottom-right
/// corner of a matrix, where movement is restricted to right and down directions.
///
/// # Arguments
///
/// * `matrix` - A 2D vector of positive integers, where each element represents
/// the cost to step on that cell.
///
/// # Returns
///
/// * `Ok(usize)` - The minimum path cost to reach the bottom-right corner from
/// the top-left corner of the matrix.
/// * `Err(MatrixError)` - An error if the matrix is empty or improperly formatted.
///
/// # Complexity
///
/// * Time complexity: `O(m * n)`, where `m` is the number of rows
/// and `n` is the number of columns in the input matrix.
/// * Space complexity: `O(n)`, as only a single row of cumulative costs
/// is stored at any time.
pub fn minimum_cost_path(matrix: Vec<Vec<usize>>) -> Result<usize, MatrixError> {
// Check if the matrix is rectangular
if !matrix.iter().all(|row| row.len() == matrix[0].len()) {
return Err(MatrixError::NonRectangularMatrix);
}

// Preprocessing the first column
for i in 1..rows {
matrix[i][0] += matrix[i - 1][0];
// Check if the matrix is empty or contains empty rows
if matrix.is_empty() || matrix.iter().all(|row| row.is_empty()) {
return Err(MatrixError::EmptyMatrix);
}

// Updating path cost for the remaining positions
// For each position, cost to reach it from top left is
// Sum of value of that position and minimum of upper and left position value
// Initialize the first row of the cost vector
let mut cost = matrix[0]
.iter()
.scan(0, |acc, &val| {
*acc += val;
Some(*acc)
})
.collect::<Vec<_>>();

for i in 1..rows {
for j in 1..columns {
matrix[i][j] += min(matrix[i - 1][j], matrix[i][j - 1]);
// Process each row from the second to the last
for row in matrix.iter().skip(1) {
// Update the first element of cost for this row
cost[0] += row[0];

// Update the rest of the elements in the current row of cost
for col in 1..matrix[0].len() {
cost[col] = row[col] + min(cost[col - 1], cost[col]);
}
}

// Return cost for bottom right element
matrix[rows - 1][columns - 1]
// The last element in cost contains the minimum path cost to the bottom-right corner
Ok(cost[matrix[0].len() - 1])
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn basic() {
// For test case in example
let matrix = vec![vec![2, 1, 4], vec![2, 1, 3], vec![3, 2, 1]];
assert_eq!(minimum_cost_path(matrix), 7);

// For a randomly generated matrix
let matrix = vec![vec![1, 2, 3], vec![4, 5, 6]];
assert_eq!(minimum_cost_path(matrix), 12);
}

#[test]
fn one_element_matrix() {
let matrix = vec![vec![2]];
assert_eq!(minimum_cost_path(matrix), 2);
}

#[test]
fn one_row() {
let matrix = vec![vec![1, 3, 2, 1, 5]];
assert_eq!(minimum_cost_path(matrix), 12);
macro_rules! minimum_cost_path_tests {
($($name:ident: $test_case:expr,)*) => {
$(
#[test]
fn $name() {
let (matrix, expected) = $test_case;
assert_eq!(minimum_cost_path(matrix), expected);
}
)*
};
}

#[test]
fn one_column() {
let matrix = vec![vec![1], vec![3], vec![2], vec![1], vec![5]];
assert_eq!(minimum_cost_path(matrix), 12);
minimum_cost_path_tests! {
basic: (
vec![
vec![2, 1, 4],
vec![2, 1, 3],
vec![3, 2, 1]
],
Ok(7)
),
single_element: (
vec![
vec![5]
],
Ok(5)
),
single_row: (
vec![
vec![1, 3, 2, 1, 5]
],
Ok(12)
),
single_column: (
vec![
vec![1],
vec![3],
vec![2],
vec![1],
vec![5]
],
Ok(12)
),
large_matrix: (
vec![
vec![1, 3, 1, 5],
vec![2, 1, 4, 2],
vec![3, 2, 1, 3],
vec![4, 3, 2, 1]
],
Ok(10)
),
uniform_matrix: (
vec![
vec![1, 1, 1],
vec![1, 1, 1],
vec![1, 1, 1]
],
Ok(5)
),
increasing_values: (
vec![
vec![1, 2, 3],
vec![4, 5, 6],
vec![7, 8, 9]
],
Ok(21)
),
high_cost_path: (
vec![
vec![1, 100, 1],
vec![1, 100, 1],
vec![1, 1, 1]
],
Ok(5)
),
complex_matrix: (
vec![
vec![5, 9, 6, 8],
vec![1, 4, 7, 3],
vec![2, 1, 8, 2],
vec![3, 6, 9, 4]
],
Ok(23)
),
empty_matrix: (
vec![],
Err(MatrixError::EmptyMatrix)
),
empty_row: (
vec![
vec![],
vec![],
vec![]
],
Err(MatrixError::EmptyMatrix)
),
non_rectangular: (
vec![
vec![1, 2, 3],
vec![4, 5],
vec![6, 7, 8]
],
Err(MatrixError::NonRectangularMatrix)
),
}
}