Import smawk 0.3.1 upstream upstream/0.3.1
authorWoohyun Jung <wh0705.jung@samsung.com>
Fri, 17 Mar 2023 06:21:49 +0000 (15:21 +0900)
committerWoohyun Jung <wh0705.jung@samsung.com>
Fri, 17 Mar 2023 06:21:49 +0000 (15:21 +0900)
14 files changed:
.cargo_vcs_info.json [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
Cargo.toml.orig [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
src/brute_force.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/monge.rs [new file with mode: 0644]
src/recursive.rs [new file with mode: 0644]
tests/agreement.rs [new file with mode: 0644]
tests/complexity.rs [new file with mode: 0644]
tests/monge.rs [new file with mode: 0644]
tests/random_monge/mod.rs [new file with mode: 0644]
tests/version-numbers.rs [new file with mode: 0644]

diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644 (file)
index 0000000..62d7460
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "git": {
+    "sha1": "7d2913e351b112c74d4d855a4979c00a47926518"
+  }
+}
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..48db409
--- /dev/null
@@ -0,0 +1,46 @@
+# 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 <martin@geisler.net>"]
+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"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644 (file)
index 0000000..b3f6dcc
--- /dev/null
@@ -0,0 +1,26 @@
+[package]
+name = "smawk"
+version = "0.3.1"
+authors = ["Martin Geisler <martin@geisler.net>"]
+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"
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..124067f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT 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.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..8568f2b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,136 @@
+# 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
diff --git a/src/brute_force.rs b/src/brute_force.rs
new file mode 100644 (file)
index 0000000..77209f9
--- /dev/null
@@ -0,0 +1,122 @@
+//! 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<T: Ord>(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<T: Ord>(matrix: &Array2<T>) -> Vec<usize> {
+    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<T: Ord>(matrix: &Array2<T>) -> Vec<usize> {
+    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);
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..3d24d3e
--- /dev/null
@@ -0,0 +1,531 @@
+//! 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<T: Copy> {
+    /// 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<T: Copy> Matrix<T> for Vec<Vec<T>> {
+    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<T: Copy> Matrix<T> for ndarray::Array2<T> {
+    #[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<T: PartialOrd + Copy, M: Matrix<T>>(matrix: &M) -> Vec<usize> {
+    // 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::<Vec<_>>(),
+        &(0..matrix.nrows()).collect::<Vec<_>>(),
+        &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<T: PartialOrd + Copy, M: Matrix<T>>(matrix: &M) -> Vec<usize> {
+    let mut minima = vec![0; matrix.ncols()];
+    smawk_inner(
+        &|i, j| matrix.index(i, j),
+        &(0..matrix.nrows()).collect::<Vec<_>>(),
+        &(0..matrix.ncols()).collect::<Vec<_>>(),
+        &mut minima,
+    );
+    minima
+}
+
+/// Compute column minima in the given area of the matrix. The
+/// `minima` slice is updated inplace.
+fn smawk_inner<T: PartialOrd + Copy, M: Fn(usize, usize) -> 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: Copy + PartialOrd, M: Fn(&[(usize, T)], usize, usize) -> 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::<Vec<_>>();
+            tentative = std::cmp::min(finished + rows.len(), size - 1);
+            let cols = (finished + 1..tentative + 1).collect::<Vec<_>>();
+            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);
+    }
+
+}
diff --git a/src/monge.rs b/src/monge.rs
new file mode 100644 (file)
index 0000000..7614a6d
--- /dev/null
@@ -0,0 +1,121 @@
+//! 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<T: Ord + Copy, M: Matrix<T>>(matrix: &M) -> bool
+where
+    Wrapping<T>: Add<Output = Wrapping<T>>,
+{
+    /// Returns `Ok(a + b)` if the computation can be done without
+    /// overflow, otherwise `Err(a + b - T::MAX - 1)` is returned.
+    fn checked_add<T: Ord + Copy>(a: Wrapping<T>, b: Wrapping<T>) -> Result<T, T>
+    where
+        Wrapping<T>: Add<Output = Wrapping<T>>,
+    {
+        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<u8>> = 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));
+    }
+}
diff --git a/src/recursive.rs b/src/recursive.rs
new file mode 100644 (file)
index 0000000..e7f769b
--- /dev/null
@@ -0,0 +1,161 @@
+//! 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<T: Ord>(matrix: &Array2<T>) -> Vec<usize> {
+    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<T: Ord>(matrix: &Array2<T>) -> Vec<usize> {
+    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<T: Ord, F: Fn() -> 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);
+    }
+}
diff --git a/tests/agreement.rs b/tests/agreement.rs
new file mode 100644 (file)
index 0000000..1d710ee
--- /dev/null
@@ -0,0 +1,105 @@
+#![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<i32> = 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<i32> = 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::<Vec<_>>();
+            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
+            );
+        }
+    }
+}
diff --git a/tests/complexity.rs b/tests/complexity.rs
new file mode 100644 (file)
index 0000000..743fe57
--- /dev/null
@@ -0,0 +1,83 @@
+#![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 <https://en.wikipedia.org/wiki/Simple_linear_regression>.
+fn linear_regression(values: &[(usize, i32)]) -> LinRegression {
+    let xs = values.iter().map(|&(x, _)| x as f64).collect::<Array1<_>>();
+    let ys = values.iter().map(|&(_, y)| y as f64).collect::<Array1<_>>();
+
+    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<i32> = 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
+    );
+}
diff --git a/tests/monge.rs b/tests/monge.rs
new file mode 100644 (file)
index 0000000..0d5a3d0
--- /dev/null
@@ -0,0 +1,83 @@
+#![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<u8> = 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<u8> = 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<u8> = 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<u8> = 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<u8> = 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]
+        ])
+    );
+}
diff --git a/tests/random_monge/mod.rs b/tests/random_monge/mod.rs
new file mode 100644 (file)
index 0000000..a913dee
--- /dev/null
@@ -0,0 +1,83 @@
+//! 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<T: PrimInt, R: Rng>(&self, m: usize, n: usize, rng: &mut R) -> Array2<T>
+    where
+        Standard: Distribution<T>,
+    {
+        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::<bool>() {
+                        row.fill(T::one())
+                    }
+                }
+            }
+            MongePrim::ConstantCols => {
+                for mut col in matrix.gencolumns_mut() {
+                    if rng.gen::<bool>() {
+                        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<R: Rng, T: PrimInt>(m: usize, n: usize, rng: &mut R) -> Array2<T>
+where
+    Standard: Distribution<T>,
+{
+    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
+}
diff --git a/tests/version-numbers.rs b/tests/version-numbers.rs
new file mode 100644 (file)
index 0000000..288592d
--- /dev/null
@@ -0,0 +1,9 @@
+#[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");
+}