Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
"ConjunctiveBooleanQuery": [Conjunctive Boolean Query],
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
"ConsecutiveOnesMatrixAugmentation": [Consecutive Ones Matrix Augmentation],
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
"SparseMatrixCompression": [Sparse Matrix Compression],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
Expand Down Expand Up @@ -6106,6 +6107,55 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("ConsecutiveOnesMatrixAugmentation")
let A = x.instance.matrix
let m = A.len()
let n = if m > 0 { A.at(0).len() } else { 0 }
let K = x.instance.bound
let perm = x.optimal_config
let A-int = A.map(row => row.map(v => if v { 1 } else { 0 }))
let reordered = A.map(row => perm.map(c => if row.at(c) { 1 } else { 0 }))
let total-flips = 0
for row in reordered {
let first = none
let last = none
let count = 0
for (j, value) in row.enumerate() {
if value == 1 {
if first == none {
first = j
}
last = j
count += 1
}
}
if first != none and last != none {
total-flips += last - first + 1 - count
}
}
[
#problem-def("ConsecutiveOnesMatrixAugmentation")[
Given an $m times n$ binary matrix $A$ and a nonnegative integer $K$, determine whether there exists a matrix $A'$, obtained from $A$ by changing at most $K$ zero entries to one, such that some permutation of the columns of $A'$ has the consecutive ones property.
][
Consecutive Ones Matrix Augmentation is problem SR16 in Garey & Johnson @garey1979. It asks whether a binary matrix can be repaired by a bounded number of augmenting flips so that every row's 1-entries become contiguous after reordering the columns. This setting appears in information retrieval and DNA physical mapping, where matrices close to the consecutive ones property can still encode useful interval structure. Booth and Lueker showed that testing whether a matrix already has the consecutive ones property is polynomial-time via PQ-trees @booth1976, but allowing bounded augmentation makes the decision problem NP-complete @booth1975. The direct exhaustive search tries all $n!$ column permutations and, for each one, computes the minimum augmentation cost by filling the holes between the first and last 1 in every row#footnote[No algorithm improving on brute-force permutation enumeration is known for the general problem in this repository's supported setting.].

*Example.* Consider the $#m times #n$ matrix $A = mat(#A-int.map(row => row.map(v => str(v)).join(", ")).join("; "))$ with $K = #K$. Under the permutation $pi = (#perm.map(p => str(p)).join(", "))$, the reordered rows are #reordered.enumerate().map(((i, row)) => [$r_#(i + 1) = (#row.map(v => str(v)).join(", "))$]).join(", "). The first row becomes $(1, 0, 1, 0, 1)$, so filling the two interior gaps yields $(1, 1, 1, 1, 1)$. The other three rows already have consecutive 1-entries under the same order, so the total augmentation cost is #total-flips and #total-flips $<= #K$, making the instance satisfiable.

#pred-commands(
"pred create --example ConsecutiveOnesMatrixAugmentation -o consecutive-ones-matrix-augmentation.json",
"pred solve consecutive-ones-matrix-augmentation.json --solver brute-force",
"pred evaluate consecutive-ones-matrix-augmentation.json --config " + x.optimal_config.map(str).join(","),
)

#figure(
align(center, math.equation([$A = #math.mat(..A-int.map(row => row.map(v => [#v])))$])),
caption: [The canonical $#m times #n$ example matrix for Consecutive Ones Matrix Augmentation. The permutation $pi = (#perm.map(p => str(p)).join(", "))$ makes only the first row need augmentation, and exactly two zero-to-one flips suffice.],
) <fig:coma-example>
]
]
}

#{
let x = load-model-example("ConsecutiveOnesSubmatrix")
let A = x.instance.matrix
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ Flags by problem type:
PartialFeedbackEdgeSet --graph, --budget, --max-cycle-length [--num-vertices]
BMF --matrix (0/1), --rank
ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k
ConsecutiveOnesMatrixAugmentation --matrix (0/1), --bound
ConsecutiveOnesSubmatrix --matrix (0/1), --k
SparseMatrixCompression --matrix (0/1), --bound
SteinerTree --graph, --edge-weights, --terminals
Expand Down
103 changes: 101 additions & 2 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use crate::util;
use anyhow::{bail, Context, Result};
use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample};
use problemreductions::models::algebraic::{
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesSubmatrix,
SparseMatrixCompression, BMF,
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesMatrixAugmentation,
ConsecutiveOnesSubmatrix, SparseMatrixCompression, BMF,
};
use problemreductions::models::formula::Quantifier;
use problemreductions::models::graph::{
Expand Down Expand Up @@ -693,6 +693,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"ConsecutiveBlockMinimization" => {
"--matrix '[[true,false,true],[false,true,true]]' --bound 2"
}
"ConsecutiveOnesMatrixAugmentation" => {
"--matrix \"1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0\" --bound 2"
}
"SparseMatrixCompression" => {
"--matrix \"1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0\" --bound 2"
}
Expand Down Expand Up @@ -739,6 +742,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
("PrimeAttributeName", "dependencies") => return "deps".to_string(),
("PrimeAttributeName", "query_attribute") => return "query".to_string(),
("MixedChinesePostman", "arc_weights") => return "arc-costs".to_string(),
("ConsecutiveOnesMatrixAugmentation", "bound") => return "bound".to_string(),
("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(),
("SparseMatrixCompression", "bound_k") => return "bound".to_string(),
("StackerCrane", "edges") => return "graph".to_string(),
Expand Down Expand Up @@ -824,6 +828,9 @@ fn help_flag_hint(
("PathConstrainedNetworkFlow", "paths") => {
"semicolon-separated arc-index paths: \"0,2,5,8;1,4,7,9\""
}
("ConsecutiveOnesMatrixAugmentation", "matrix") => {
"semicolon-separated 0/1 rows: \"1,0;0,1\""
}
("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
("SparseMatrixCompression", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => {
Expand Down Expand Up @@ -2817,6 +2824,22 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// ConsecutiveOnesMatrixAugmentation
"ConsecutiveOnesMatrixAugmentation" => {
let matrix = parse_bool_matrix(args)?;
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"ConsecutiveOnesMatrixAugmentation requires --matrix and --bound\n\n\
Usage: pred create ConsecutiveOnesMatrixAugmentation --matrix \"1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0\" --bound 2"
)
})?;
(
ser(ConsecutiveOnesMatrixAugmentation::try_new(matrix, bound)
.map_err(|e| anyhow::anyhow!(e))?)?,
resolved_variant.clone(),
)
}

// SparseMatrixCompression
"SparseMatrixCompression" => {
let matrix = parse_bool_matrix(args)?;
Expand Down Expand Up @@ -7994,4 +8017,80 @@ mod tests {
let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("bound >= 1"));
}

#[test]
fn test_create_consecutive_ones_matrix_augmentation_json() {
use crate::dispatch::ProblemJsonOutput;

let mut args = empty_args();
args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string());
args.matrix = Some("1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0".to_string());
args.bound = Some(2);

let output_path =
std::env::temp_dir().join(format!("coma-create-{}.json", std::process::id()));
let out = OutputConfig {
output: Some(output_path.clone()),
quiet: true,
json: false,
auto_json: false,
};

create(&args, &out).unwrap();

let json = std::fs::read_to_string(&output_path).unwrap();
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
assert_eq!(created.problem_type, "ConsecutiveOnesMatrixAugmentation");
assert!(created.variant.is_empty());
assert_eq!(
created.data,
serde_json::json!({
"matrix": [
[true, false, false, true, true],
[true, true, false, false, false],
[false, true, true, false, true],
[false, false, true, true, false],
],
"bound": 2,
})
);

let _ = std::fs::remove_file(output_path);
}

#[test]
fn test_create_consecutive_ones_matrix_augmentation_requires_bound() {
let mut args = empty_args();
args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string());
args.matrix = Some("1,0;0,1".to_string());

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("ConsecutiveOnesMatrixAugmentation requires --matrix and --bound"));
assert!(err.contains("Usage: pred create ConsecutiveOnesMatrixAugmentation"));
}

#[test]
fn test_create_consecutive_ones_matrix_augmentation_negative_bound() {
let mut args = empty_args();
args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string());
args.matrix = Some("1,0;0,1".to_string());
args.bound = Some(-1);

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("nonnegative"));
}
}
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ pub mod variant;
/// Prelude module for convenient imports.
pub mod prelude {
// Problem types
pub use crate::models::algebraic::{QuadraticAssignment, SparseMatrixCompression, BMF, QUBO};
pub use crate::models::algebraic::{
ConsecutiveOnesMatrixAugmentation, QuadraticAssignment, SparseMatrixCompression, BMF, QUBO,
};
pub use crate::models::formula::{
CNFClause, CircuitSAT, KSatisfiability, NAESatisfiability, QuantifiedBooleanFormulas,
Satisfiability,
Expand Down
163 changes: 163 additions & 0 deletions src/models/algebraic/consecutive_ones_matrix_augmentation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! Consecutive Ones Matrix Augmentation problem implementation.
//!
//! Given an m x n binary matrix A and a nonnegative integer K, determine
//! whether there exists a permutation of the columns and at most K zero-to-one
//! augmentations such that every row has consecutive 1s.

use crate::registry::{FieldInfo, ProblemSchemaEntry};
use crate::traits::{Problem, SatisfactionProblem};
use serde::{Deserialize, Serialize};

inventory::submit! {
ProblemSchemaEntry {
name: "ConsecutiveOnesMatrixAugmentation",
display_name: "Consecutive Ones Matrix Augmentation",
aliases: &[],
dimensions: &[],
module_path: module_path!(),
description: "Augment a binary matrix with at most K zero-to-one flips so some column permutation has the consecutive ones property",
fields: &[
FieldInfo { name: "matrix", type_name: "Vec<Vec<bool>>", description: "m x n binary matrix A" },
FieldInfo { name: "bound", type_name: "i64", description: "Upper bound K on zero-to-one augmentations" },
],
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsecutiveOnesMatrixAugmentation {
matrix: Vec<Vec<bool>>,
bound: i64,
}

impl ConsecutiveOnesMatrixAugmentation {
pub fn new(matrix: Vec<Vec<bool>>, bound: i64) -> Self {
Self::try_new(matrix, bound).unwrap_or_else(|err| panic!("{err}"))
}

pub fn try_new(matrix: Vec<Vec<bool>>, bound: i64) -> Result<Self, String> {
let num_cols = matrix.first().map_or(0, Vec::len);
if matrix.iter().any(|row| row.len() != num_cols) {
return Err("all matrix rows must have the same length".to_string());
}
if bound < 0 {
return Err("bound must be nonnegative".to_string());
}
Ok(Self { matrix, bound })
}

pub fn matrix(&self) -> &[Vec<bool>] {
&self.matrix
}

pub fn bound(&self) -> i64 {
self.bound
}

pub fn num_rows(&self) -> usize {
self.matrix.len()
}

pub fn num_cols(&self) -> usize {
self.matrix.first().map_or(0, Vec::len)
}

fn validate_permutation(&self, config: &[usize]) -> bool {
if config.len() != self.num_cols() {
return false;
}

let mut seen = vec![false; self.num_cols()];
for &col in config {
if col >= self.num_cols() || seen[col] {
return false;
}
seen[col] = true;
}
true
}

fn row_augmentation_cost(row: &[bool], config: &[usize]) -> usize {
let mut first_one = None;
let mut last_one = None;
let mut one_count = 0usize;

for (position, &col) in config.iter().enumerate() {
if row[col] {
first_one.get_or_insert(position);
last_one = Some(position);
one_count += 1;
}
}

match (first_one, last_one) {
(Some(first), Some(last)) => last - first + 1 - one_count,
_ => 0,
}
}

fn total_augmentation_cost(&self, config: &[usize]) -> Option<usize> {
if !self.validate_permutation(config) {
return None;
}

let mut total = 0usize;
for row in &self.matrix {
total += Self::row_augmentation_cost(row, config);
if total > self.bound as usize {
return Some(total);
}
}

Some(total)
}
}

impl Problem for ConsecutiveOnesMatrixAugmentation {
const NAME: &'static str = "ConsecutiveOnesMatrixAugmentation";
type Metric = bool;

fn dims(&self) -> Vec<usize> {
vec![self.num_cols(); self.num_cols()]
}

fn evaluate(&self, config: &[usize]) -> bool {
self.total_augmentation_cost(config)
.is_some_and(|cost| cost <= self.bound as usize)
}

fn variant() -> Vec<(&'static str, &'static str)> {
crate::variant_params![]
}

fn num_variables(&self) -> usize {
self.num_cols()
}
}

impl SatisfactionProblem for ConsecutiveOnesMatrixAugmentation {}

crate::declare_variants! {
default sat ConsecutiveOnesMatrixAugmentation => "factorial(num_cols) * num_rows * num_cols",
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
vec![crate::example_db::specs::ModelExampleSpec {
id: "consecutive_ones_matrix_augmentation",
instance: Box::new(ConsecutiveOnesMatrixAugmentation::new(
vec![
vec![true, false, false, true, true],
vec![true, true, false, false, false],
vec![false, true, true, false, true],
vec![false, false, true, true, false],
],
2,
)),
optimal_config: vec![0, 1, 4, 2, 3],
optimal_value: serde_json::json!(true),
}]
}

#[cfg(test)]
#[path = "../../unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs"]
mod tests;
Loading
Loading