-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* ref: refactor minimum cost path * feat: add custom error types
- Loading branch information
Showing
1 changed file
with
156 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
), | ||
} | ||
} |