smawk-0.3.1/.cargo_vcs_info.json0000644000000001121400530612400122020ustar { "git": { "sha1": "7d2913e351b112c74d4d855a4979c00a47926518" } } smawk-0.3.1/Cargo.toml0000644000000025151400530612400102110ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "smawk" version = "0.3.1" authors = ["Martin Geisler "] exclude = [".github/", ".gitignore", "benches/", "examples/"] description = "Functions for finding row-minima in a totally monotone matrix." readme = "README.md" keywords = ["smawk", "matrix", "optimization", "dynamic-programming"] categories = ["algorithms", "science"] license = "MIT" repository = "https://github.com/mgeisler/smawk" [dependencies.ndarray] version = "0.14" optional = true [dev-dependencies.num-traits] version = "0.2" [dev-dependencies.rand] version = "0.8" [dev-dependencies.rand_chacha] version = "0.3" [dev-dependencies.version-sync] version = "0.9" [badges.appveyor] repository = "mgeisler/smawk" [badges.codecov] repository = "mgeisler/smawk" [badges.travis-ci] repository = "mgeisler/smawk" smawk-0.3.1/Cargo.toml.orig010064400017510000165000000013541400530610200136770ustar 00000000000000[package] name = "smawk" version = "0.3.1" authors = ["Martin Geisler "] description = "Functions for finding row-minima in a totally monotone matrix." repository = "https://github.com/mgeisler/smawk" readme = "README.md" keywords = ["smawk", "matrix", "optimization", "dynamic-programming"] categories = ["algorithms", "science"] license = "MIT" edition = "2018" exclude = [".github/", ".gitignore", "benches/", "examples/"] [badges] travis-ci = { repository = "mgeisler/smawk" } appveyor = { repository = "mgeisler/smawk" } codecov = { repository = "mgeisler/smawk" } [dependencies] ndarray = { version = "0.14", optional = true } [dev-dependencies] version-sync = "0.9" num-traits = "0.2" rand = "0.8" rand_chacha = "0.3" smawk-0.3.1/LICENSE010064400017510000165000000020571400530610200120160ustar 00000000000000MIT License Copyright (c) 2017 Martin Geisler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. smawk-0.3.1/README.md010064400017510000165000000116661400530610200122760ustar 00000000000000# SMAWK Algorithm in Rust [![](https://github.com/mgeisler/smawk/workflows/build/badge.svg)][build-status] [![](https://codecov.io/gh/mgeisler/smawk/branch/master/graph/badge.svg)][codecov] [![](https://img.shields.io/crates/v/smawk.svg)][crates-io] [![](https://docs.rs/smawk/badge.svg)][api-docs] This crate contains an implementation of the [SMAWK algorithm][smawk] for finding the smallest element per row in a totally monotone matrix. The SMAWK algorithm allows you to lower the running time of some algorithms from O(*n*²) to just O(*n*). In other words, you can turn a quadratic time complexity (which is often too expensive) into linear time complexity. Finding optimal line breaks in a paragraph of text is an example of an algorithm which would normally take O(*n*²) time for *n* words. With this crate, the running time becomes linear. Please see the [textwrap crate][textwrap] for an example of this. ## Usage Add this to your `Cargo.toml`: ```toml [dependencies] smawk = "0.3" ``` You can now efficiently find row and column minima. Here is an example where we find the column minima: ```rust use smawk::smawk_column_minima; let matrix = vec![ vec![3, 2, 4, 5, 6], vec![2, 1, 3, 3, 4], vec![2, 1, 3, 3, 4], vec![3, 2, 4, 3, 4], vec![4, 3, 2, 1, 1], ]; let minima = vec![1, 1, 4, 4, 4]; assert_eq!(smawk_column_minima(&matrix), minima); ``` The `minima` vector gives the index of the minimum value per column, so `minima[0] == 1` since the minimum value in the first column is 2 (row 1). Note that the smallest row index is returned. ### Cargo Features This crate has an optional dependency on the [`ndarray` crate](https://docs.rs/ndarray/), which provides an efficient matrix implementation. Enable the `ndarray` Cargo feature to use it. ## Documentation **[API documentation][api-docs]** ## Changelog ### Version 0.3.1 (2021-01-30) This release relaxes the bounds on the `smawk_row_minima`, `smawk_column_minima`, and `online_column_minima` functions so that they work on matrices containing floating point numbers. * [#55](https://github.com/mgeisler/smawk/pull/55): Relax bounds to `PartialOrd` instead of `Ord`. * [#56](https://github.com/mgeisler/smawk/pull/56): Update dependencies to their latest versions. * [#59](https://github.com/mgeisler/smawk/pull/59): Give an example of what SMAWK does in the README. ### Version 0.3.0 (2020-09-02) This release slims down the crate significantly by making `ndarray` an optional dependency. * [#45](https://github.com/mgeisler/smawk/pull/45): Move non-SMAWK code and unit tests out of lib and into separate modules. * [#46](https://github.com/mgeisler/smawk/pull/46): Switch `smawk_row_minima` and `smawk_column_minima` functions to a new `Matrix` trait. * [#47](https://github.com/mgeisler/smawk/pull/47): Make the dependency on the `ndarray` crate optional. * [#48](https://github.com/mgeisler/smawk/pull/48): Let `is_monge` take a `Matrix` argument instead of `ndarray::Array2`. * [#50](https://github.com/mgeisler/smawk/pull/50): Remove mandatory dependencies on `rand` and `num-traits` crates. ### Version 0.2.0 (2020-07-29) This release updates the code to Rust 2018. * [#18](https://github.com/mgeisler/smawk/pull/18): Make `online_column_minima` generic in matrix type. * [#23](https://github.com/mgeisler/smawk/pull/23): Switch to the [Rust 2018][rust-2018] edition. We test against the latest stable and nightly version of Rust. * [#29](https://github.com/mgeisler/smawk/pull/29): Drop strict Rust 2018 compatibility by not testing with Rust 1.31.0. * [#32](https://github.com/mgeisler/smawk/pull/32): Fix crash on overflow in `is_monge`. * [#33](https://github.com/mgeisler/smawk/pull/33): Update `rand` dependency to latest version and get rid of `rand_derive`. * [#34](https://github.com/mgeisler/smawk/pull/34): Bump `num-traits` and `version-sync` dependencies to latest versions. * [#35](https://github.com/mgeisler/smawk/pull/35): Drop unnecessary Windows tests. The assumption is that the numeric computations we do are cross-platform. * [#36](https://github.com/mgeisler/smawk/pull/36): Update `ndarray` dependency to the latest version. * [#37](https://github.com/mgeisler/smawk/pull/37): Automate publishing new releases to crates.io. ### Version 0.1.0 — August 7th, 2018 First release with the classical offline SMAWK algorithm as well as a newer online version where the matrix entries can depend on previously computed column minima. ## License SMAWK can be distributed according to the [MIT license][mit]. Contributions will be accepted under the same license. [build-status]: https://github.com/mgeisler/smawk/actions?query=branch%3Amaster+workflow%3Abuild [crates-io]: https://crates.io/crates/smawk [codecov]: https://codecov.io/gh/mgeisler/smawk [textwrap]: https://crates.io/crates/textwrap [smawk]: https://en.wikipedia.org/wiki/SMAWK_algorithm [api-docs]: https://docs.rs/smawk/ [rust-2018]: https://doc.rust-lang.org/edition-guide/rust-2018/ [mit]: LICENSE smawk-0.3.1/src/brute_force.rs010064400017510000165000000063511400530610200144460ustar 00000000000000//! Brute-force algorithm for finding column minima. //! //! The functions here are mostly meant to be used for testing //! correctness of the SMAWK implementation. //! //! **Note: this module is only available if you enable the `ndarray` //! Cargo feature.** use ndarray::{Array2, ArrayView1}; /// Compute lane minimum by brute force. /// /// This does a simple scan through the lane (row or column). #[inline] pub fn lane_minimum(lane: ArrayView1<'_, T>) -> usize { lane.iter() .enumerate() .min_by_key(|&(idx, elem)| (elem, idx)) .map(|(idx, _)| idx) .expect("empty lane in matrix") } /// Compute row minima by brute force in O(*mn*) time. /// /// # Panics /// /// It is an error to call this on a matrix with zero columns. pub fn row_minima(matrix: &Array2) -> Vec { matrix.genrows().into_iter().map(lane_minimum).collect() } /// Compute column minima by brute force in O(*mn*) time. /// /// # Panics /// /// It is an error to call this on a matrix with zero rows. pub fn column_minima(matrix: &Array2) -> Vec { matrix.gencolumns().into_iter().map(lane_minimum).collect() } #[cfg(test)] mod tests { use super::*; use ndarray::arr2; #[test] fn brute_force_1x1() { let matrix = arr2(&[[2]]); let minima = vec![0]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn brute_force_2x1() { let matrix = arr2(&[ [3], // [2], ]); let minima = vec![0, 0]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn brute_force_1x2() { let matrix = arr2(&[[2, 1]]); let minima = vec![1]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn brute_force_2x2() { let matrix = arr2(&[ [3, 2], // [2, 1], ]); let minima = vec![1, 1]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn brute_force_3x3() { let matrix = arr2(&[ [3, 4, 4], // [3, 4, 4], [2, 3, 3], ]); let minima = vec![0, 0, 0]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn brute_force_4x4() { let matrix = arr2(&[ [4, 5, 5, 5], // [2, 3, 3, 3], [2, 3, 3, 3], [2, 2, 2, 2], ]); let minima = vec![0, 0, 0, 0]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn brute_force_5x5() { let matrix = arr2(&[ [3, 2, 4, 5, 6], [2, 1, 3, 3, 4], [2, 1, 3, 3, 4], [3, 2, 4, 3, 4], [4, 3, 2, 1, 1], ]); let minima = vec![1, 1, 1, 1, 3]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } } smawk-0.3.1/src/lib.rs010064400017510000165000000405361400530610200127200ustar 00000000000000//! This crate implements various functions that help speed up dynamic //! programming, most importantly the SMAWK algorithm for finding row //! or column minima in a totally monotone matrix with *m* rows and //! *n* columns in time O(*m* + *n*). This is much better than the //! brute force solution which would take O(*mn*). When *m* and *n* //! are of the same order, this turns a quadratic function into a //! linear function. //! //! # Examples //! //! Computing the column minima of an *m* ✕ *n* Monge matrix can be //! done efficiently with `smawk_column_minima`: //! //! ``` //! use smawk::{Matrix, smawk_column_minima}; //! //! let matrix = vec![ //! vec![3, 2, 4, 5, 6], //! vec![2, 1, 3, 3, 4], //! vec![2, 1, 3, 3, 4], //! vec![3, 2, 4, 3, 4], //! vec![4, 3, 2, 1, 1], //! ]; //! let minima = vec![1, 1, 4, 4, 4]; //! assert_eq!(smawk_column_minima(&matrix), minima); //! ``` //! //! The `minima` vector gives the index of the minimum value per //! column, so `minima[0] == 1` since the minimum value in the first //! column is 2 (row 1). Note that the smallest row index is returned. //! //! # Definitions //! //! Some of the functions in this crate only work on matrices that are //! *totally monotone*, which we will define below. //! //! ## Monotone Matrices //! //! We start with a helper definition. Given an *m* ✕ *n* matrix `M`, //! we say that `M` is *monotone* when the minimum value of row `i` is //! found to the left of the minimum value in row `i'` where `i < i'`. //! //! More formally, if we let `rm(i)` denote the column index of the //! left-most minimum value in row `i`, then we have //! //! ```text //! rm(0) ≤ rm(1) ≤ ... ≤ rm(m - 1) //! ``` //! //! This means that as you go down the rows from top to bottom, the //! row-minima proceed from left to right. //! //! The algorithms in this crate deal with finding such row- and //! column-minima. //! //! ## Totally Monotone Matrices //! //! We say that a matrix `M` is *totally monotone* when every //! sub-matrix is monotone. A sub-matrix is formed by the intersection //! of any two rows `i < i'` and any two columns `j < j'`. //! //! This is often expressed as via this equivalent condition: //! //! ```text //! M[i, j] > M[i, j'] => M[i', j] > M[i', j'] //! ``` //! //! for all `i < i'` and `j < j'`. //! //! ## Monge Property for Matrices //! //! A matrix `M` is said to fulfill the *Monge property* if //! //! ```text //! M[i, j] + M[i', j'] ≤ M[i, j'] + M[i', j] //! ``` //! //! for all `i < i'` and `j < j'`. This says that given any rectangle //! in the matrix, the sum of the top-left and bottom-right corners is //! less than or equal to the sum of the bottom-left and upper-right //! corners. //! //! All Monge matrices are totally monotone, so it is enough to //! establish that the Monge property holds in order to use a matrix //! with the functions in this crate. If your program is dealing with //! unknown inputs, it can use [`monge::is_monge`] to verify that a //! matrix is a Monge matrix. #![doc(html_root_url = "https://docs.rs/smawk/0.3.1")] #[cfg(feature = "ndarray")] pub mod brute_force; pub mod monge; #[cfg(feature = "ndarray")] pub mod recursive; /// Minimal matrix trait for two-dimensional arrays. /// /// This provides the functionality needed to represent a read-only /// numeric matrix. You can query the size of the matrix and access /// elements. Modeled after [`ndarray::Array2`] from the [ndarray /// crate](https://crates.io/crates/ndarray). /// /// Enable the `ndarray` Cargo feature if you want to use it with /// `ndarray::Array2`. pub trait Matrix { /// Return the number of rows. fn nrows(&self) -> usize; /// Return the number of columns. fn ncols(&self) -> usize; /// Return a matrix element. fn index(&self, row: usize, column: usize) -> T; } /// Simple and inefficient matrix representation used for doctest /// examples and simple unit tests. /// /// You should prefer implementing it yourself, or you can enable the /// `ndarray` Cargo feature and use the provided implementation for /// [`ndarray::Array2`]. impl Matrix for Vec> { fn nrows(&self) -> usize { self.len() } fn ncols(&self) -> usize { self[0].len() } fn index(&self, row: usize, column: usize) -> T { self[row][column] } } /// Adapting [`ndarray::Array2`] to the `Matrix` trait. /// /// **Note: this implementation is only available if you enable the /// `ndarray` Cargo feature.** #[cfg(feature = "ndarray")] impl Matrix for ndarray::Array2 { #[inline] fn nrows(&self) -> usize { self.nrows() } #[inline] fn ncols(&self) -> usize { self.ncols() } #[inline] fn index(&self, row: usize, column: usize) -> T { self[[row, column]] } } /// Compute row minima in O(*m* + *n*) time. /// /// This implements the SMAWK algorithm for finding row minima in a /// totally monotone matrix. /// /// The SMAWK algorithm is from Agarwal, Klawe, Moran, Shor, and /// Wilbur, *Geometric applications of a matrix searching algorithm*, /// Algorithmica 2, pp. 195-208 (1987) and the code here is a /// translation [David Eppstein's Python code][pads]. /// /// [pads]: https://github.com/jfinkels/PADS/blob/master/pads/smawk.py /// /// Running time on an *m* ✕ *n* matrix: O(*m* + *n*). /// /// # Panics /// /// It is an error to call this on a matrix with zero columns. pub fn smawk_row_minima>(matrix: &M) -> Vec { // Benchmarking shows that SMAWK performs roughly the same on row- // and column-major matrices. let mut minima = vec![0; matrix.nrows()]; smawk_inner( &|j, i| matrix.index(i, j), &(0..matrix.ncols()).collect::>(), &(0..matrix.nrows()).collect::>(), &mut minima, ); minima } /// Compute column minima in O(*m* + *n*) time. /// /// This implements the SMAWK algorithm for finding column minima in a /// totally monotone matrix. /// /// The SMAWK algorithm is from Agarwal, Klawe, Moran, Shor, and /// Wilbur, *Geometric applications of a matrix searching algorithm*, /// Algorithmica 2, pp. 195-208 (1987) and the code here is a /// translation [David Eppstein's Python code][pads]. /// /// [pads]: https://github.com/jfinkels/PADS/blob/master/pads/smawk.py /// /// Running time on an *m* ✕ *n* matrix: O(*m* + *n*). /// /// # Panics /// /// It is an error to call this on a matrix with zero rows. pub fn smawk_column_minima>(matrix: &M) -> Vec { let mut minima = vec![0; matrix.ncols()]; smawk_inner( &|i, j| matrix.index(i, j), &(0..matrix.nrows()).collect::>(), &(0..matrix.ncols()).collect::>(), &mut minima, ); minima } /// Compute column minima in the given area of the matrix. The /// `minima` slice is updated inplace. fn smawk_inner T>( matrix: &M, rows: &[usize], cols: &[usize], mut minima: &mut [usize], ) { if cols.is_empty() { return; } let mut stack = Vec::with_capacity(cols.len()); for r in rows { // TODO: use stack.last() instead of stack.is_empty() etc while !stack.is_empty() && matrix(stack[stack.len() - 1], cols[stack.len() - 1]) > matrix(*r, cols[stack.len() - 1]) { stack.pop(); } if stack.len() != cols.len() { stack.push(*r); } } let rows = &stack; let mut odd_cols = Vec::with_capacity(1 + cols.len() / 2); for (idx, c) in cols.iter().enumerate() { if idx % 2 == 1 { odd_cols.push(*c); } } smawk_inner(matrix, rows, &odd_cols, &mut minima); let mut r = 0; for (c, &col) in cols.iter().enumerate().filter(|(c, _)| c % 2 == 0) { let mut row = rows[r]; let last_row = if c == cols.len() - 1 { rows[rows.len() - 1] } else { minima[cols[c + 1]] }; let mut pair = (matrix(row, col), row); while row != last_row { r += 1; row = rows[r]; if (matrix(row, col), row) < pair { pair = (matrix(row, col), row); } } minima[col] = pair.1; } } /// Compute upper-right column minima in O(*m* + *n*) time. /// /// The input matrix must be totally monotone. /// /// The function returns a vector of `(usize, T)`. The `usize` in the /// tuple at index `j` tells you the row of the minimum value in /// column `j` and the `T` value is minimum value itself. /// /// The algorithm only considers values above the main diagonal, which /// means that it computes values `v(j)` where: /// /// ```text /// v(0) = initial /// v(j) = min { M[i, j] | i < j } for j > 0 /// ``` /// /// If we let `r(j)` denote the row index of the minimum value in /// column `j`, the tuples in the result vector become `(r(j), M[r(j), /// j])`. /// /// The algorithm is an *online* algorithm, in the sense that `matrix` /// function can refer back to previously computed column minima when /// determining an entry in the matrix. The guarantee is that we only /// call `matrix(i, j)` after having computed `v(i)`. This is /// reflected in the `&[(usize, T)]` argument to `matrix`, which grows /// as more and more values are computed. pub fn online_column_minima T>( initial: T, size: usize, matrix: M, ) -> Vec<(usize, T)> { let mut result = vec![(0, initial)]; // State used by the algorithm. let mut finished = 0; let mut base = 0; let mut tentative = 0; // Shorthand for evaluating the matrix. We need a macro here since // we don't want to borrow the result vector. macro_rules! m { ($i:expr, $j:expr) => {{ assert!($i < $j, "(i, j) not above diagonal: ({}, {})", $i, $j); assert!( $i < size && $j < size, "(i, j) out of bounds: ({}, {}), size: {}", $i, $j, size ); matrix(&result[..finished + 1], $i, $j) }}; } // Keep going until we have finished all size columns. Since the // columns are zero-indexed, we're done when finished == size - 1. while finished < size - 1 { // First case: we have already advanced past the previous // tentative value. We make a new tentative value by applying // smawk_inner to the largest square submatrix that fits under // the base. let i = finished + 1; if i > tentative { let rows = (base..finished + 1).collect::>(); tentative = std::cmp::min(finished + rows.len(), size - 1); let cols = (finished + 1..tentative + 1).collect::>(); let mut minima = vec![0; tentative + 1]; smawk_inner(&|i, j| m![i, j], &rows, &cols, &mut minima); for col in cols { let row = minima[col]; let v = m![row, col]; if col >= result.len() { result.push((row, v)); } else if v < result[col].1 { result[col] = (row, v); } } finished = i; continue; } // Second case: the new column minimum is on the diagonal. All // subsequent ones will be at least as low, so we can clear // out all our work from higher rows. As in the fourth case, // the loss of tentative is amortized against the increase in // base. let diag = m![i - 1, i]; if diag < result[i].1 { result[i] = (i - 1, diag); base = i - 1; tentative = i; finished = i; continue; } // Third case: row i-1 does not supply a column minimum in any // column up to tentative. We simply advance finished while // maintaining the invariant. if m![i - 1, tentative] >= result[tentative].1 { finished = i; continue; } // Fourth and final case: a new column minimum at tentative. // This allows us to make progress by incorporating rows prior // to finished into the base. The base invariant holds because // these rows cannot supply any later column minima. The work // done when we last advanced tentative (and undone by this // step) can be amortized against the increase in base. base = i - 1; tentative = i; finished = i; } result } #[cfg(test)] mod tests { use super::*; #[test] fn smawk_1x1() { let matrix = vec![vec![2]]; assert_eq!(smawk_row_minima(&matrix), vec![0]); assert_eq!(smawk_column_minima(&matrix), vec![0]); } #[test] fn smawk_2x1() { let matrix = vec![ vec![3], // vec![2], ]; assert_eq!(smawk_row_minima(&matrix), vec![0, 0]); assert_eq!(smawk_column_minima(&matrix), vec![1]); } #[test] fn smawk_1x2() { let matrix = vec![vec![2, 1]]; assert_eq!(smawk_row_minima(&matrix), vec![1]); assert_eq!(smawk_column_minima(&matrix), vec![0, 0]); } #[test] fn smawk_2x2() { let matrix = vec![ vec![3, 2], // vec![2, 1], ]; assert_eq!(smawk_row_minima(&matrix), vec![1, 1]); assert_eq!(smawk_column_minima(&matrix), vec![1, 1]); } #[test] fn smawk_3x3() { let matrix = vec![ vec![3, 4, 4], // vec![3, 4, 4], vec![2, 3, 3], ]; assert_eq!(smawk_row_minima(&matrix), vec![0, 0, 0]); assert_eq!(smawk_column_minima(&matrix), vec![2, 2, 2]); } #[test] fn smawk_4x4() { let matrix = vec![ vec![4, 5, 5, 5], // vec![2, 3, 3, 3], vec![2, 3, 3, 3], vec![2, 2, 2, 2], ]; assert_eq!(smawk_row_minima(&matrix), vec![0, 0, 0, 0]); assert_eq!(smawk_column_minima(&matrix), vec![1, 3, 3, 3]); } #[test] fn smawk_5x5() { let matrix = vec![ vec![3, 2, 4, 5, 6], vec![2, 1, 3, 3, 4], vec![2, 1, 3, 3, 4], vec![3, 2, 4, 3, 4], vec![4, 3, 2, 1, 1], ]; assert_eq!(smawk_row_minima(&matrix), vec![1, 1, 1, 1, 3]); assert_eq!(smawk_column_minima(&matrix), vec![1, 1, 4, 4, 4]); } #[test] fn online_1x1() { let matrix = vec![vec![0]]; let minima = vec![(0, 0)]; assert_eq!(online_column_minima(0, 1, |_, i, j| matrix[i][j]), minima); } #[test] fn online_2x2() { let matrix = vec![ vec![0, 2], // vec![0, 0], ]; let minima = vec![(0, 0), (0, 2)]; assert_eq!(online_column_minima(0, 2, |_, i, j| matrix[i][j]), minima); } #[test] fn online_3x3() { let matrix = vec![ vec![0, 4, 4], // vec![0, 0, 4], vec![0, 0, 0], ]; let minima = vec![(0, 0), (0, 4), (0, 4)]; assert_eq!(online_column_minima(0, 3, |_, i, j| matrix[i][j]), minima); } #[test] fn online_4x4() { let matrix = vec![ vec![0, 5, 5, 5], // vec![0, 0, 3, 3], vec![0, 0, 0, 3], vec![0, 0, 0, 0], ]; let minima = vec![(0, 0), (0, 5), (1, 3), (1, 3)]; assert_eq!(online_column_minima(0, 4, |_, i, j| matrix[i][j]), minima); } #[test] fn online_5x5() { let matrix = vec![ vec![0, 2, 4, 6, 7], vec![0, 0, 3, 4, 5], vec![0, 0, 0, 3, 4], vec![0, 0, 0, 0, 4], vec![0, 0, 0, 0, 0], ]; let minima = vec![(0, 0), (0, 2), (1, 3), (2, 3), (2, 4)]; assert_eq!(online_column_minima(0, 5, |_, i, j| matrix[i][j]), minima); } #[test] fn smawk_works_with_partial_ord() { let matrix = vec![ vec![3.0, 2.0], // vec![2.0, 1.0], ]; assert_eq!(smawk_row_minima(&matrix), vec![1, 1]); assert_eq!(smawk_column_minima(&matrix), vec![1, 1]); } #[test] fn online_works_with_partial_ord() { let matrix = vec![ vec![0.0, 2.0], // vec![0.0, 0.0], ]; let minima = vec![(0, 0.0), (0, 2.0)]; assert_eq!(online_column_minima(0.0, 2, |_, i:usize, j:usize| matrix[i][j]), minima); } } smawk-0.3.1/src/monge.rs010064400017510000165000000070021400530610200132460ustar 00000000000000//! Functions for generating and checking Monge arrays. //! //! The functions here are mostly meant to be used for testing //! correctness of the SMAWK implementation. use crate::Matrix; use std::num::Wrapping; use std::ops::Add; /// Verify that a matrix is a Monge matrix. /// /// A [Monge matrix] \(or array) is a matrix where the following /// inequality holds: /// /// ```text /// M[i, j] + M[i', j'] <= M[i, j'] + M[i', j] for all i < i', j < j' /// ``` /// /// The inequality says that the sum of the main diagonal is less than /// the sum of the antidiagonal. Checking this condition is done by /// checking *n* ✕ *m* submatrices, so the running time is O(*mn*). /// /// [Monge matrix]: https://en.wikipedia.org/wiki/Monge_array pub fn is_monge>(matrix: &M) -> bool where Wrapping: Add>, { /// Returns `Ok(a + b)` if the computation can be done without /// overflow, otherwise `Err(a + b - T::MAX - 1)` is returned. fn checked_add(a: Wrapping, b: Wrapping) -> Result where Wrapping: Add>, { let sum = a + b; if sum < a { Err(sum.0) } else { Ok(sum.0) } } (0..matrix.nrows() - 1) .flat_map(|row| (0..matrix.ncols() - 1).map(move |col| (row, col))) .all(|(row, col)| { let top_left = Wrapping(matrix.index(row, col)); let top_right = Wrapping(matrix.index(row, col + 1)); let bot_left = Wrapping(matrix.index(row + 1, col)); let bot_right = Wrapping(matrix.index(row + 1, col + 1)); match ( checked_add(top_left, bot_right), checked_add(bot_left, top_right), ) { (Ok(a), Ok(b)) => a <= b, // No overflow. (Err(a), Err(b)) => a <= b, // Double overflow. (Ok(_), Err(_)) => true, // Antidiagonal overflow. (Err(_), Ok(_)) => false, // Main diagonal overflow. } }) } #[cfg(test)] mod tests { use super::*; #[test] fn is_monge_handles_overflow() { // The x + y <= z + w computations will overflow for an u8 // matrix unless is_monge is careful. let matrix: Vec> = vec![ vec![200, 200, 200, 200], vec![200, 200, 200, 200], vec![200, 200, 200, 200], ]; assert!(is_monge(&matrix)); } #[test] fn monge_constant_rows() { let matrix = vec![ vec![42, 42, 42, 42], vec![0, 0, 0, 0], vec![100, 100, 100, 100], vec![1000, 1000, 1000, 1000], ]; assert!(is_monge(&matrix)); } #[test] fn monge_constant_cols() { let matrix = vec![ vec![42, 0, 100, 1000], vec![42, 0, 100, 1000], vec![42, 0, 100, 1000], vec![42, 0, 100, 1000], ]; assert!(is_monge(&matrix)); } #[test] fn monge_upper_right() { let matrix = vec![ vec![10, 10, 42, 42, 42], vec![10, 10, 42, 42, 42], vec![10, 10, 10, 10, 10], vec![10, 10, 10, 10, 10], ]; assert!(is_monge(&matrix)); } #[test] fn monge_lower_left() { let matrix = vec![ vec![10, 10, 10, 10, 10], vec![10, 10, 10, 10, 10], vec![42, 42, 42, 10, 10], vec![42, 42, 42, 10, 10], ]; assert!(is_monge(&matrix)); } } smawk-0.3.1/src/recursive.rs010064400017510000165000000110331400530610200141470ustar 00000000000000//! Recursive algorithm for finding column minima. //! //! The functions here are mostly meant to be used for testing //! correctness of the SMAWK implementation. //! //! **Note: this module is only available if you enable the `ndarray` //! Cargo feature.** use ndarray::{s, Array2, ArrayView2, Axis}; /// Compute row minima in O(*m* + *n* log *m*) time. /// /// # Panics /// /// It is an error to call this on a matrix with zero columns. pub fn row_minima(matrix: &Array2) -> Vec { let mut minima = vec![0; matrix.nrows()]; recursive_inner(matrix.view(), &|| Direction::Row, 0, &mut minima); minima } /// Compute column minima in O(*n* + *m* log *n*) time. /// /// # Panics /// /// It is an error to call this on a matrix with zero rows. pub fn column_minima(matrix: &Array2) -> Vec { let mut minima = vec![0; matrix.ncols()]; recursive_inner(matrix.view(), &|| Direction::Column, 0, &mut minima); minima } /// The type of minima (row or column) we compute. enum Direction { Row, Column, } /// Compute the minima along the given direction (`Direction::Row` for /// row minima and `Direction::Column` for column minima). /// /// The direction is given as a generic function argument to allow /// monomorphization to kick in. The function calls will be inlined /// and optimized away and the result is that the compiler generates /// differnet code for finding row and column minima. fn recursive_inner Direction>( matrix: ArrayView2<'_, T>, dir: &F, offset: usize, minima: &mut [usize], ) { if matrix.is_empty() { return; } let axis = match dir() { Direction::Row => Axis(0), Direction::Column => Axis(1), }; let mid = matrix.len_of(axis) / 2; let min_idx = crate::brute_force::lane_minimum(matrix.index_axis(axis, mid)); minima[mid] = offset + min_idx; if mid == 0 { return; // Matrix has a single row or column, so we're done. } let top_left = match dir() { Direction::Row => matrix.slice(s![..mid, ..(min_idx + 1)]), Direction::Column => matrix.slice(s![..(min_idx + 1), ..mid]), }; let bot_right = match dir() { Direction::Row => matrix.slice(s![(mid + 1).., min_idx..]), Direction::Column => matrix.slice(s![min_idx.., (mid + 1)..]), }; recursive_inner(top_left, dir, offset, &mut minima[..mid]); recursive_inner(bot_right, dir, offset + min_idx, &mut minima[mid + 1..]); } #[cfg(test)] mod tests { use super::*; use ndarray::arr2; #[test] fn recursive_1x1() { let matrix = arr2(&[[2]]); let minima = vec![0]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn recursive_2x1() { let matrix = arr2(&[ [3], // [2], ]); let minima = vec![0, 0]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn recursive_1x2() { let matrix = arr2(&[[2, 1]]); let minima = vec![1]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn recursive_2x2() { let matrix = arr2(&[ [3, 2], // [2, 1], ]); let minima = vec![1, 1]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn recursive_3x3() { let matrix = arr2(&[ [3, 4, 4], // [3, 4, 4], [2, 3, 3], ]); let minima = vec![0, 0, 0]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn recursive_4x4() { let matrix = arr2(&[ [4, 5, 5, 5], // [2, 3, 3, 3], [2, 3, 3, 3], [2, 2, 2, 2], ]); let minima = vec![0, 0, 0, 0]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } #[test] fn recursive_5x5() { let matrix = arr2(&[ [3, 2, 4, 5, 6], [2, 1, 3, 3, 4], [2, 1, 3, 3, 4], [3, 2, 4, 3, 4], [4, 3, 2, 1, 1], ]); let minima = vec![1, 1, 1, 1, 3]; assert_eq!(row_minima(&matrix), minima); assert_eq!(column_minima(&matrix.reversed_axes()), minima); } } smawk-0.3.1/tests/agreement.rs010064400017510000165000000077101400530610200144710ustar 00000000000000#![cfg(feature = "ndarray")] use ndarray::{s, Array2}; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use smawk::{brute_force, recursive}; use smawk::{online_column_minima, smawk_column_minima, smawk_row_minima}; mod random_monge; use random_monge::random_monge_matrix; /// Check that the brute force, recursive, and SMAWK functions /// give identical results on a large number of randomly generated /// Monge matrices. #[test] fn column_minima_agree() { let sizes = vec![1, 2, 3, 4, 5, 10, 15, 20, 30]; let mut rng = ChaCha20Rng::seed_from_u64(0); for _ in 0..4 { for m in sizes.clone().iter() { for n in sizes.clone().iter() { let matrix: Array2 = random_monge_matrix(*m, *n, &mut rng); // Compute and test row minima. let brute_force = brute_force::row_minima(&matrix); let recursive = recursive::row_minima(&matrix); let smawk = smawk_row_minima(&matrix); assert_eq!( brute_force, recursive, "recursive and brute force differs on:\n{:?}", matrix ); assert_eq!( brute_force, smawk, "SMAWK and brute force differs on:\n{:?}", matrix ); // Do the same for the column minima. let brute_force = brute_force::column_minima(&matrix); let recursive = recursive::column_minima(&matrix); let smawk = smawk_column_minima(&matrix); assert_eq!( brute_force, recursive, "recursive and brute force differs on:\n{:?}", matrix ); assert_eq!( brute_force, smawk, "SMAWK and brute force differs on:\n{:?}", matrix ); } } } } /// Check that the brute force and online SMAWK functions give /// identical results on a large number of randomly generated /// Monge matrices. #[test] fn online_agree() { let sizes = vec![1, 2, 3, 4, 5, 10, 15, 20, 30, 50]; let mut rng = ChaCha20Rng::seed_from_u64(0); for _ in 0..5 { for &size in &sizes { // Random totally monotone square matrix of the // desired size. let mut matrix: Array2 = random_monge_matrix(size, size, &mut rng); // Adjust matrix so the column minima are above the // diagonal. The brute_force::column_minima will still // work just fine on such a mangled Monge matrix. let max = *matrix.iter().max().unwrap_or(&0); for idx in 0..(size as isize) { // Using the maximum value of the matrix instead // of i32::max_value() makes for prettier matrices // in case we want to print them. matrix.slice_mut(s![idx..idx + 1, ..idx + 1]).fill(max); } // The online algorithm always returns the initial // value for the left-most column -- without // inspecting the column at all. So we fill the // left-most column with this value to have the brute // force algorithm do the same. let initial = 42; matrix.slice_mut(s![0.., ..1]).fill(initial); // Brute-force computation of column minima, returned // in the same form as online_column_minima. let brute_force = brute_force::column_minima(&matrix) .iter() .enumerate() .map(|(j, &i)| (i, matrix[[i, j]])) .collect::>(); let online = online_column_minima(initial, size, |_, i, j| matrix[[i, j]]); assert_eq!( brute_force, online, "brute force and online differ on:\n{:3?}", matrix ); } } } smawk-0.3.1/tests/complexity.rs010064400017510000165000000044631400530610200147210ustar 00000000000000#![cfg(feature = "ndarray")] use ndarray::{Array1, Array2}; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use smawk::online_column_minima; mod random_monge; use random_monge::random_monge_matrix; #[derive(Debug)] struct LinRegression { alpha: f64, beta: f64, r_squared: f64, } /// Square an expression. Works equally well for floats and matrices. macro_rules! squared { ($x:expr) => { $x * $x }; } /// Compute the mean of a 1-dimensional array. macro_rules! mean { ($a:expr) => { $a.mean().expect("Mean of empty array") }; } /// Compute a simple linear regression from the list of values. /// /// See . fn linear_regression(values: &[(usize, i32)]) -> LinRegression { let xs = values.iter().map(|&(x, _)| x as f64).collect::>(); let ys = values.iter().map(|&(_, y)| y as f64).collect::>(); let xs_mean = mean!(&xs); let ys_mean = mean!(&ys); let xs_ys_mean = mean!(&xs * &ys); let cov_xs_ys = ((&xs - xs_mean) * (&ys - ys_mean)).scalar_sum(); let var_xs = squared!(&xs - xs_mean).scalar_sum(); let beta = cov_xs_ys / var_xs; let alpha = ys_mean - beta * xs_mean; let r_squared = squared!(xs_ys_mean - xs_mean * ys_mean) / ((mean!(&xs * &xs) - squared!(xs_mean)) * (mean!(&ys * &ys) - squared!(ys_mean))); LinRegression { alpha: alpha, beta: beta, r_squared: r_squared, } } /// Check that the number of matrix accesses in `online_column_minima` /// grows as O(*n*) for *n* ✕ *n* matrix. #[test] fn online_linear_complexity() { let mut rng = ChaCha20Rng::seed_from_u64(0); let mut data = vec![]; for &size in &[1, 2, 3, 4, 5, 10, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100] { let matrix: Array2 = random_monge_matrix(size, size, &mut rng); let count = std::cell::RefCell::new(0); online_column_minima(0, size, |_, i, j| { *count.borrow_mut() += 1; matrix[[i, j]] }); data.push((size, count.into_inner())); } let lin_reg = linear_regression(&data); assert!( lin_reg.r_squared > 0.95, "r² = {:.4} is lower than expected for a linear fit\nData points: {:?}\n{:?}", lin_reg.r_squared, data, lin_reg ); } smawk-0.3.1/tests/monge.rs010064400017510000165000000040351400530610200136240ustar 00000000000000#![cfg(feature = "ndarray")] use ndarray::{arr2, Array, Array2}; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use smawk::monge::is_monge; mod random_monge; use random_monge::{random_monge_matrix, MongePrim}; #[test] fn random_monge() { let mut rng = ChaCha20Rng::seed_from_u64(0); let matrix: Array2 = random_monge_matrix(5, 5, &mut rng); assert!(is_monge(&matrix)); assert_eq!( matrix, arr2(&[ [2, 3, 4, 4, 5], [5, 5, 6, 6, 7], [3, 3, 4, 4, 5], [5, 2, 3, 3, 4], [5, 2, 3, 3, 4] ]) ); } #[test] fn monge_constant_rows() { let mut rng = ChaCha20Rng::seed_from_u64(0); let matrix: Array2 = MongePrim::ConstantRows.to_matrix(5, 4, &mut rng); assert!(is_monge(&matrix)); for row in matrix.genrows() { let elem = row[0]; assert_eq!(row, Array::from_elem(matrix.ncols(), elem)); } } #[test] fn monge_constant_cols() { let mut rng = ChaCha20Rng::seed_from_u64(0); let matrix: Array2 = MongePrim::ConstantCols.to_matrix(5, 4, &mut rng); assert!(is_monge(&matrix)); for column in matrix.gencolumns() { let elem = column[0]; assert_eq!(column, Array::from_elem(matrix.nrows(), elem)); } } #[test] fn monge_upper_right_ones() { let mut rng = ChaCha20Rng::seed_from_u64(1); let matrix: Array2 = MongePrim::UpperRightOnes.to_matrix(5, 4, &mut rng); assert!(is_monge(&matrix)); assert_eq!( matrix, arr2(&[ [0, 0, 1, 1], [0, 0, 1, 1], [0, 0, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0] ]) ); } #[test] fn monge_lower_left_ones() { let mut rng = ChaCha20Rng::seed_from_u64(1); let matrix: Array2 = MongePrim::LowerLeftOnes.to_matrix(5, 4, &mut rng); assert!(is_monge(&matrix)); assert_eq!( matrix, arr2(&[ [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 0, 0], [1, 1, 0, 0], [1, 1, 0, 0] ]) ); } smawk-0.3.1/tests/random_monge/mod.rs010064400017510000165000000051171400530610200157450ustar 00000000000000//! Test functionality for generating random Monge matrices. // The code is put here so we can reuse it in different integration // tests, without Cargo finding it when `cargo test` is run. See the // section on "Submodules in Integration Tests" in // https://doc.rust-lang.org/book/ch11-03-test-organization.html use ndarray::{s, Array2}; use num_traits::PrimInt; use rand::distributions::{Distribution, Standard}; use rand::Rng; /// A Monge matrix can be decomposed into one of these primitive /// building blocks. #[derive(Copy, Clone)] pub enum MongePrim { ConstantRows, ConstantCols, UpperRightOnes, LowerLeftOnes, } impl MongePrim { /// Generate a Monge matrix from a primitive. pub fn to_matrix(&self, m: usize, n: usize, rng: &mut R) -> Array2 where Standard: Distribution, { let mut matrix = Array2::from_elem((m, n), T::zero()); // Avoid panic in UpperRightOnes and LowerLeftOnes below. if m == 0 || n == 0 { return matrix; } match *self { MongePrim::ConstantRows => { for mut row in matrix.genrows_mut() { if rng.gen::() { row.fill(T::one()) } } } MongePrim::ConstantCols => { for mut col in matrix.gencolumns_mut() { if rng.gen::() { col.fill(T::one()) } } } MongePrim::UpperRightOnes => { let i = rng.gen_range(0..(m + 1) as isize); let j = rng.gen_range(0..(n + 1) as isize); matrix.slice_mut(s![..i, -j..]).fill(T::one()); } MongePrim::LowerLeftOnes => { let i = rng.gen_range(0..(m + 1) as isize); let j = rng.gen_range(0..(n + 1) as isize); matrix.slice_mut(s![-i.., ..j]).fill(T::one()); } } matrix } } /// Generate a random Monge matrix. pub fn random_monge_matrix(m: usize, n: usize, rng: &mut R) -> Array2 where Standard: Distribution, { let monge_primitives = [ MongePrim::ConstantRows, MongePrim::ConstantCols, MongePrim::LowerLeftOnes, MongePrim::UpperRightOnes, ]; let mut matrix = Array2::from_elem((m, n), T::zero()); for _ in 0..(m + n) { let monge = monge_primitives[rng.gen_range(0..monge_primitives.len())]; matrix = matrix + monge.to_matrix(m, n, rng); } matrix } smawk-0.3.1/tests/version-numbers.rs010064400017510000165000000003041400530610200156500ustar 00000000000000#[test] fn test_readme_deps() { version_sync::assert_markdown_deps_updated!("README.md"); } #[test] fn test_html_root_url() { version_sync::assert_html_root_url_updated!("src/lib.rs"); }